Mail: render HTML emails faithfully (sender layout + CSS) in a strict sandbox, with an image proxy and Load images #726

Open
opened 2026-10-02 12:42:21 +00:00 by kayg · 29 comments
Owner

Owner report (2026-10-02)

"Why aren't HTML emails rendered as they are?" A registrar's HTML newsletter shows as plain themed text: app font, app ink on the app background, no layout, colours, logos or buttons.

Cause

crates/plugins/mail/src/html.rs keeps "a small content vocabulary": it removes <style>, every style attribute, images, SVG, tables' presentation and more. apps/web/src/lib/mail/frame.ts then applies the app's font, ink and colour scheme. That was a privacy-first first slice; it is not acceptable as the reading experience.

Expected (owner: render as they are)

  1. Faithful rendering by default. Keep the sender's HTML layout and CSS: <style> blocks, inline style, tables, bgcolor/width/align attributes, web fonts only from allowed sources, and media queries. Show it the way mainstream mail clients do, on its own surface (the sender's background, or white when none is set), inside the reader card.
  2. Security stays strict: it renders in a sandboxed iframe with no allow-scripts, no same-origin, no forms, no navigation (links open via our controlled handler), and a strict CSP. Sanitise CSS instead of deleting it: drop expression(), behavior, -moz-binding, @import, and position: fixed overlays that escape the frame. Remote url() in CSS is treated like remote images.
  3. Privacy: remote images and CSS backgrounds load only after "Load images" (per message, with "Always for this sender"). They load through a server-side image proxy (no cookies, no Referer, the User's IP hidden, size and type limits, cached), so the sender never sees the User's IP or the open time until the User chooses to load. Inline (cid:) images and data: images under a size cap show at once.
  4. Dark mode: by default, keep the sender's colours on their own light surface inside the dark app. Offer a per-message toggle "Adapt to dark mode" that applies a safe colour inversion only when the email has no dark-mode CSS of its own; respect prefers-color-scheme rules in the email.
  5. Plain-text emails keep the current themed rendering.
  6. Speed: sanitise once at ingest/cache time (store the sanitised HTML), render from cache; no visible reflow when the frame sizes to its content (#640's sizing).

Tests

  • Fixtures (synthetic, tests only) of typical newsletter, receipt and transactional layouts. Screenshot comparisons in light and dark.
  • Robustness: the existing hostile-HTML tests keep passing. Add CSS-based tracking (background:url(...), @font-face remote), CSS escape attempts, huge tables, deeply nested tables, and 10 MB HTML.
  • Privacy: no network request leaves the browser for remote content until the User loads images. The proxy strips cookies and the Referer.
## Owner report (2026-10-02) "Why aren't HTML emails rendered as they are?" A registrar's HTML newsletter shows as plain themed text: app font, app ink on the app background, no layout, colours, logos or buttons. ## Cause `crates/plugins/mail/src/html.rs` keeps "a small content vocabulary": it removes `<style>`, every `style` attribute, images, SVG, tables' presentation and more. `apps/web/src/lib/mail/frame.ts` then applies the app's font, ink and colour scheme. That was a privacy-first first slice; it is not acceptable as the reading experience. ## Expected (owner: render as they are) 1. **Faithful rendering by default.** Keep the sender's HTML layout and CSS: `<style>` blocks, inline `style`, tables, `bgcolor`/`width`/`align` attributes, web fonts only from allowed sources, and media queries. Show it the way mainstream mail clients do, on its own surface (the sender's background, or white when none is set), inside the reader card. 2. **Security stays strict:** it renders in a sandboxed iframe with no `allow-scripts`, no same-origin, no forms, no navigation (links open via our controlled handler), and a strict CSP. Sanitise CSS instead of deleting it: drop `expression()`, `behavior`, `-moz-binding`, `@import`, and `position: fixed` overlays that escape the frame. Remote `url()` in CSS is treated like remote images. 3. **Privacy:** remote images and CSS backgrounds load only after "Load images" (per message, with "Always for this sender"). They load through a server-side image proxy (no cookies, no Referer, the User's IP hidden, size and type limits, cached), so the sender never sees the User's IP or the open time until the User chooses to load. Inline (`cid:`) images and `data:` images under a size cap show at once. 4. **Dark mode:** by default, keep the sender's colours on their own light surface inside the dark app. Offer a per-message toggle "Adapt to dark mode" that applies a safe colour inversion only when the email has no dark-mode CSS of its own; respect `prefers-color-scheme` rules in the email. 5. Plain-text emails keep the current themed rendering. 6. Speed: sanitise once at ingest/cache time (store the sanitised HTML), render from cache; no visible reflow when the frame sizes to its content (#640's sizing). ## Tests - Fixtures (synthetic, tests only) of typical newsletter, receipt and transactional layouts. Screenshot comparisons in light and dark. - Robustness: the existing hostile-HTML tests keep passing. Add CSS-based tracking (`background:url(...)`, `@font-face` remote), CSS escape attempts, huge tables, deeply nested tables, and 10 MB HTML. - Privacy: no network request leaves the browser for remote content until the User loads images. The proxy strips cookies and the Referer.
Author
Owner

Owner direction (2026-10-02): the product must be normie-friendly; privacy features work silently in the background. This changes points 3 and 4:

  • Remote images load automatically, always through the server-side image proxy (no cookies, no Referer, the User's IP hidden, images fetched at delivery/first sync time, not at open time, so open-tracking learns nothing). Known tracking pixels (1×1, known tracker hosts) are dropped silently. There is no 'Remote images are blocked' banner and no 'Load images' button by default. A single Settings → Mail switch 'Load remote content' (default on) is the only control; per-sender rules are not needed.
  • No security vocabulary in the reader: no 'sandboxed', no 'blocked' notices. If something is removed for safety, the email simply renders without it.
  • Dark mode: show the email as the sender designed it on its own surface; offer 'Adapt to dark mode' only from the ⋯ menu, not as a banner.
    Everything else in the issue stands.
**Owner direction (2026-10-02): the product must be normie-friendly; privacy features work silently in the background.** This changes points 3 and 4: - **Remote images load automatically**, always through the server-side image proxy (no cookies, no Referer, the User's IP hidden, images fetched at delivery/first sync time, not at open time, so open-tracking learns nothing). Known tracking pixels (1×1, known tracker hosts) are dropped silently. There is **no 'Remote images are blocked' banner and no 'Load images' button** by default. A single Settings → Mail switch 'Load remote content' (default on) is the only control; per-sender rules are not needed. - No security vocabulary in the reader: no 'sandboxed', no 'blocked' notices. If something is removed for safety, the email simply renders without it. - Dark mode: show the email as the sender designed it on its own surface; offer 'Adapt to dark mode' only from the ⋯ menu, not as a banner. Everything else in the issue stands.
Author
Owner

Started #726 on job/mailhtml-726, base c4a61e8cf0. Read the latest owner comment: remote content defaults on, fetch through server at sync time, no reader consent notice. Integrated job/maillayouts (#640); retained current shared keyboard motion rules when resolving conflicts.

Started #726 on job/mailhtml-726, base c4a61e8cf090170f35b1bed3350d9de20c83ecd5. Read the latest owner comment: remote content defaults on, fetch through server at sync time, no reader consent notice. Integrated job/maillayouts (#640); retained current shared keyboard motion rules when resolving conflicts.
Author
Owner

The rev-consistency review (#427), on origin/dev at c4a61e8cf090170f35b1bed3350d9de20c83ecd5, found a privacy-flow conflict to include in #726's shared fix:

apps/web/src/lib/mail/MailView.svelte:538-539 shows “Remote images are blocked.” with a “Load images” button in the main reader. Line 546 adds “Images load from this sender.” The owner job instructions dated 2026-10-02 say privacy protections work silently by default: no warning banners, no “blocked” notice or extra click for the normal experience. The current #726 body still asks for an opt-in click.

Apply the newer owner instruction when implementing #726: protect normal image loading in the background and move any plain-language image preference to Settings with a safe default. Do not change data access controls or remove the protection. Test that the reader renders the normal image experience without a notice/click, that browser requests stay on the Instance, and that the setting is honoured. This review did not perform network or hostile-HTML tests and makes no privacy-network claim. No new duplicate issue was filed.

The rev-consistency review (#427), on `origin/dev` at `c4a61e8cf090170f35b1bed3350d9de20c83ecd5`, found a privacy-flow conflict to include in #726's shared fix: `apps/web/src/lib/mail/MailView.svelte:538-539` shows “Remote images are blocked.” with a “Load images” button in the main reader. Line 546 adds “Images load from this sender.” The owner job instructions dated 2026-10-02 say privacy protections work silently by default: no warning banners, no “blocked” notice or extra click for the normal experience. The current #726 body still asks for an opt-in click. Apply the newer owner instruction when implementing #726: protect normal image loading in the background and move any plain-language image preference to Settings with a safe default. Do not change data access controls or remove the protection. Test that the reader renders the normal image experience without a notice/click, that browser requests stay on the Instance, and that the setting is honoured. This review did not perform network or hostile-HTML tests and makes no privacy-network claim. No new duplicate issue was filed.
Author
Owner

Reader slice committed: visible message frames now use sandbox="" and a data-only image CSP. Removed the remote-image banner, sender controls and explanatory image notices. Adapt to dark mode is in the message menu and its state is deep-linkable (?adapt=dark). Plain text retains the app's theme.

Decision: #640's natural height cannot be read from an opaque, script-free iframe. A separate hidden inert sizing frame has only allow-same-origin, the same restrictive CSP, and no scripts. The visible frame has no grants. This keeps CSS in separate documents and prevents network requests even if sanitizing regresses. The hidden sizing grant needs owner confirmation at final review.

Web slice checks:

svelte-check found 0 errors and 0 warnings
Test Files  155 passed (155)
Tests  1057 passed (1057)

The two pre-existing frame expectations for themed HTML and direct HTTPS images changed because #726 explicitly replaces those behaviors. New expectations require sender surfaces and embedded image bytes.

Reader slice committed: visible message frames now use sandbox="" and a data-only image CSP. Removed the remote-image banner, sender controls and explanatory image notices. Adapt to dark mode is in the message menu and its state is deep-linkable (?adapt=dark). Plain text retains the app's theme. Decision: #640's natural height cannot be read from an opaque, script-free iframe. A separate hidden inert sizing frame has only allow-same-origin, the same restrictive CSP, and no scripts. The visible frame has no grants. This keeps CSS in separate documents and prevents network requests even if sanitizing regresses. The hidden sizing grant needs owner confirmation at final review. Web slice checks: ``` svelte-check found 0 errors and 0 warnings Test Files 155 passed (155) Tests 1057 passed (1057) ``` The two pre-existing frame expectations for themed HTML and direct HTTPS images changed because #726 explicitly replaces those behaviors. New expectations require sender surfaces and embedded image bytes.
Author
Owner

Root causes confirmed on 2f4482ded0: subscription rows have no session identity; pending_pushes does not check Auth; versioned thumbnail 200/304 responses use private, max-age=31536000, immutable; imageResolver returns external HTTP(S) URLs directly. The queued readerCache on job/mailhtml-726 has no clear/cancellation path. A new push regression failed because disablePush skips unsubscribe after a 401; the local cleanup fix now passes all four push tests.

Decisions: bind push to the existing hash-only Auth session key without changing the public request context fields. Keep thumbnail URL versions and ETags, but send private, no-store for both 200 and 304 (#765 explicitly changes the old cache-policy expectation). No proxy exists on #726 yet; add one shared server route using the existing calternal-plugin outbound resolver, with pinned DNS, bounded raster responses and no browser notice. Provide the queued Mail cache fix as compatible source in this branch; do not merge the unrelated Mail UI branch.

Root causes confirmed on 2f4482ded066d9c5d9c59130377907f7fd2916c9: subscription rows have no session identity; pending_pushes does not check Auth; versioned thumbnail 200/304 responses use private, max-age=31536000, immutable; imageResolver returns external HTTP(S) URLs directly. The queued readerCache on job/mailhtml-726 has no clear/cancellation path. A new push regression failed because disablePush skips unsubscribe after a 401; the local cleanup fix now passes all four push tests. Decisions: bind push to the existing hash-only Auth session key without changing the public request context fields. Keep thumbnail URL versions and ETags, but send private, no-store for both 200 and 304 (#765 explicitly changes the old cache-policy expectation). No proxy exists on #726 yet; add one shared server route using the existing calternal-plugin outbound resolver, with pinned DNS, bounded raster responses and no browser notice. Provide the queued Mail cache fix as compatible source in this branch; do not merge the unrelated Mail UI branch.
Author
Owner

Browserfix found that the pinned job/mailhtml-726 tree has no server image-proxy route yet, while Notes currently sends external image URLs directly to the browser. Browserfix is adding a single authenticated /api/v1/images/proxy route with the existing SSRF guard, redirect-hop revalidation, no Cookie or Referer forwarding, and bounded type/size/time/concurrency. Please reuse this route for Mail remote images; no second proxy is needed.

Browserfix found that the pinned `job/mailhtml-726` tree has no server image-proxy route yet, while Notes currently sends external image URLs directly to the browser. Browserfix is adding a single authenticated `/api/v1/images/proxy` route with the existing SSRF guard, redirect-hop revalidation, no Cookie or Referer forwarding, and bounded type/size/time/concurrency. Please reuse this route for Mail remote images; no second proxy is needed.
Author
Owner

Committed reader cache invalidation: clearing bodies also detaches in-flight requests, so a late response cannot restore content after a preference change. Regression test: 4 passed; web check: svelte-check found 0 errors and 0 warnings.

The new HTML layout test failed against the original sanitizer with <table><tbody><tr><td>Welcome</td></tr></tbody></table>: sender style, class and width were removed. The replacement uses parsed CSS and bounded embedded raster content. Rust's cold build took about 50 minutes on this shared host; the proxy dependencies are now building. No host-idle wait is being used.

Committed reader cache invalidation: clearing bodies also detaches in-flight requests, so a late response cannot restore content after a preference change. Regression test: 4 passed; web check: `svelte-check found 0 errors and 0 warnings`. The new HTML layout test failed against the original sanitizer with `<table><tbody><tr><td>Welcome</td></tr></tbody></table>`: sender style, class and width were removed. The replacement uses parsed CSS and bounded embedded raster content. Rust's cold build took about 50 minutes on this shared host; the proxy dependencies are now building. No host-idle wait is being used.
Author
Owner

The cached HTML policy now preserves prepared bodies during repeated provider UID/flag updates only when both raw HTML and local/CID presentation match. The content worker compares raw HTML and the pending policy version again before writing, so a concurrent sync cannot receive an older rendering. Tests cover repeated sync and changed CID content. Newly delivered content is refreshed in the open reader by one bounded background timer.

The shared compiler cache repeatedly stalls requests and falls back after its server stops. This worktree's remaining Cargo commands bypass that wrapper; no shared cache or other job was stopped. Production web build succeeded. The new local performance fixture includes a 768×768 real raster close to the compressed-byte limit and about 64 KiB HTML, plus a cached-image burst. Measurements will be labelled local shared host.

The earlier image-consent and strip-CSS expectations in mail_api.mjs are changed because #726 explicitly replaces them: default-on User preference, retained safe styling, no direct sender URLs, and no reader banner or sender-consent action. Other assertions remain unchanged.

The cached HTML policy now preserves prepared bodies during repeated provider UID/flag updates only when both raw HTML and local/CID presentation match. The content worker compares raw HTML and the pending policy version again before writing, so a concurrent sync cannot receive an older rendering. Tests cover repeated sync and changed CID content. Newly delivered content is refreshed in the open reader by one bounded background timer. The shared compiler cache repeatedly stalls requests and falls back after its server stops. This worktree's remaining Cargo commands bypass that wrapper; no shared cache or other job was stopped. Production web build succeeded. The new local performance fixture includes a 768×768 real raster close to the compressed-byte limit and about 64 KiB HTML, plus a cached-image burst. Measurements will be labelled local shared host. The earlier image-consent and strip-CSS expectations in mail_api.mjs are changed because #726 explicitly replaces them: default-on User preference, retained safe styling, no direct sender URLs, and no reader banner or sender-consent action. Other assertions remain unchanged.
Author
Owner

First Rust run: 52 passed; 2 failed; 2 ignored. Every hostile HTML, newsletter, cache-race, redirect/header and cross-User proxy test passed. The failures were the exact old table inventory (the new image cache is intentional in #726) and a version-8 migration fixture that used the current writer, which now requires the version-10 columns. The fixture now seeds only the historical columns directly; the original duplicate-UID, flag and row-preservation assertions remain unchanged. The inventory adds only mail_remote_images.

Production newsletter screenshots will use the real content worker to prepare their inline test images and CSS, rather than injecting pre-sanitized HTML into the frame. Cached-image ownership fixtures remain separate from those rendering rows.

First Rust run: `52 passed; 2 failed; 2 ignored`. Every hostile HTML, newsletter, cache-race, redirect/header and cross-User proxy test passed. The failures were the exact old table inventory (the new image cache is intentional in #726) and a version-8 migration fixture that used the current writer, which now requires the version-10 columns. The fixture now seeds only the historical columns directly; the original duplicate-UID, flag and row-preservation assertions remain unchanged. The inventory adds only mail_remote_images. Production newsletter screenshots will use the real content worker to prepare their inline test images and CSS, rather than injecting pre-sanitized HTML into the frame. Cached-image ownership fixtures remain separate from those rendering rows.
Author
Owner

Delivery wakes two bounded deduplication slots. This prevents a new delivery's wake from disappearing into an already running content job after that job selected its batch. A regression leases the first job, repeats delivery, completes it and proves a distinct successor is still ready. The final write compares both raw HTML and the original CID map, so same-HTML/new-CID sync cannot receive stale images.

origin/dev was fetched and merged once at 32338690ad. Its Mail migrations stop at 0009, so this branch's 0010 is free. The merge changed only media-runtime setup. calternal-api clippy and its 9 tests pass; Mail clippy and the remaining crate gates are running.

Browser plugin not available. The production review uses the repository Playwright harness. Flow: open a Mail message → render prepared newsletter → use its links and dark adaptation → change Settings → Mail remote content and Undo. Screenshots cover macOS rendering, 390/820/1440 px, light/dark and all three newsletters. The existing cross-User matrix gains an owned cached-image fixture and a focused proxy-route run with real installation sessions; missing/foreign response profiles are compared.

Delivery wakes two bounded deduplication slots. This prevents a new delivery's wake from disappearing into an already running content job after that job selected its batch. A regression leases the first job, repeats delivery, completes it and proves a distinct successor is still ready. The final write compares both raw HTML and the original CID map, so same-HTML/new-CID sync cannot receive stale images. origin/dev was fetched and merged once at 32338690ade441bea45f62d1baa64b87c607cb08. Its Mail migrations stop at 0009, so this branch's 0010 is free. The merge changed only media-runtime setup. calternal-api clippy and its 9 tests pass; Mail clippy and the remaining crate gates are running. Browser plugin not available. The production review uses the repository Playwright harness. Flow: open a Mail message → render prepared newsletter → use its links and dark adaptation → change Settings → Mail remote content and Undo. Screenshots cover macOS rendering, 390/820/1440 px, light/dark and all three newsletters. The existing cross-User matrix gains an owned cached-image fixture and a focused proxy-route run with real installation sessions; missing/foreign response profiles are compared.
Author
Owner

The first post-merge web suite reported 18 failures across 11 component files. Every failure is Error: Test timed out in 5000ms.; there are no failed assertions. The same suite is running with two workers and a 30-second timeout, with no changed expectations. The initial Mail clippy run found three collapsible conditions and test-module order; these are fixed without changing behavior. API clippy and 9 API tests passed.

Link controls now have at least 44×44 px targets. Work is bounded by 256 link occurrences, eight wrapped fragments each, and 1024 total parent controls. The sanitizer retains sender IDs inside isolated documents for CSS selectors and collects up to 256 distinct destinations. The focused frame tests pass (2/2).

Public image redirects are handled manually, up to three hops, with all-address validation and DNS pinning on each hop. The fetch deadline is shared across hops. The private-redirect transport regression remains. A decoder retains the concurrency permit even if its async prefetch is cancelled; blocking decodes therefore cannot silently exceed the four-fetch cap.

The first post-merge web suite reported 18 failures across 11 component files. Every failure is `Error: Test timed out in 5000ms.`; there are no failed assertions. The same suite is running with two workers and a 30-second timeout, with no changed expectations. The initial Mail clippy run found three collapsible conditions and test-module order; these are fixed without changing behavior. API clippy and 9 API tests passed. Link controls now have at least 44×44 px targets. Work is bounded by 256 link occurrences, eight wrapped fragments each, and 1024 total parent controls. The sanitizer retains sender IDs inside isolated documents for CSS selectors and collects up to 256 distinct destinations. The focused frame tests pass (2/2). Public image redirects are handled manually, up to three hops, with all-address validation and DNS pinning on each hop. The fetch deadline is shared across hops. The private-redirect transport regression remains. A decoder retains the concurrency permit even if its async prefetch is cancelled; blocking decodes therefore cannot silently exceed the four-fetch cap.
Author
Owner

The read-only security reviewer found no confirmed critical issue. It found a CSS child-selector regression, incomplete declared-pixel detection, lifetime cache exhaustion, disabled-account image reads and case-sensitive response MIME matching.

Fixes now in the worktree: preserve CSS > combinators and test them; count CSS nesting outside strings/comments; inspect parsed width/height declarations (including priorities) and simple class/ID stylesheet selectors before prefetch; require enabled Connected Accounts at image reads; compare image MIME types without case; reclaim oldest derived image entries when an account approaches 64 MiB or 10,000 rows. Reclamation leaves 8 MiB and 1,000 rows for new Mail, preserves source messages, and never opens an upstream request on a reader action. Old reclaimed images remain absent until a future sync prepares changed content. This is a bounded-cache decision, not specified by DESIGN §45. A regression test covers negative-entry reclamation and account isolation.

Generated contracts now describe the cache route and global preference. The production browser probe covers 18 newsletter screenshots and 6 Settings screenshots, all macOS at 390/820/1440 px in light/dark, native link activation with keyboard/pointer/touch, Undo, the existing focused cross-User matrix and a local hot-path profile. Runtime evidence is pending the cold server build. API gates pass; web tests pass with two workers and 30-second test timeouts. Mail/server/calendar gates are queued on the target lock.

The read-only security reviewer found no confirmed critical issue. It found a CSS child-selector regression, incomplete declared-pixel detection, lifetime cache exhaustion, disabled-account image reads and case-sensitive response MIME matching. Fixes now in the worktree: preserve CSS `>` combinators and test them; count CSS nesting outside strings/comments; inspect parsed width/height declarations (including priorities) and simple class/ID stylesheet selectors before prefetch; require enabled Connected Accounts at image reads; compare image MIME types without case; reclaim oldest derived image entries when an account approaches 64 MiB or 10,000 rows. Reclamation leaves 8 MiB and 1,000 rows for new Mail, preserves source messages, and never opens an upstream request on a reader action. Old reclaimed images remain absent until a future sync prepares changed content. This is a bounded-cache decision, not specified by DESIGN §45. A regression test covers negative-entry reclamation and account isolation. Generated contracts now describe the cache route and global preference. The production browser probe covers 18 newsletter screenshots and 6 Settings screenshots, all macOS at 390/820/1440 px in light/dark, native link activation with keyboard/pointer/touch, Undo, the existing focused cross-User matrix and a local hot-path profile. Runtime evidence is pending the cold server build. API gates pass; web tests pass with two workers and 30-second test timeouts. Mail/server/calendar gates are queued on the target lock.
Author
Owner

Source review found that the new Mail frame read .dark, while the shared root publishes .is-dark and data-mode. That mismatch kept sender dark CSS and the optional dark adaptation on the light scheme.

A regression used a real HTML root with the shared flags. Before the fix, Vitest reported AssertionError: expected 'light' to be 'dark'. After reading the shared root flags, the focused run reports:

 Test Files  1 passed (1)
      Tests  3 passed (3)

The existing two assertions remain unchanged. The new test also checks sender dark CSS and the optional invert rule. A final web check and production rebuild are running. The cold server build is linking, but shared HDD reads have kept the linker in an I/O wait for more than eighteen minutes. Mail/calendar/server gates remain behind its target lock; screenshots, the real-server probes and the performance profile cannot run until the binary exists. No result is claimed for those pending checks.

Source review found that the new Mail frame read `.dark`, while the shared root publishes `.is-dark` and `data-mode`. That mismatch kept sender dark CSS and the optional dark adaptation on the light scheme. A regression used a real HTML root with the shared flags. Before the fix, Vitest reported `AssertionError: expected 'light' to be 'dark'`. After reading the shared root flags, the focused run reports: ``` Test Files 1 passed (1) Tests 3 passed (3) ``` The existing two assertions remain unchanged. The new test also checks sender dark CSS and the optional invert rule. A final web check and production rebuild are running. The cold server build is linking, but shared HDD reads have kept the linker in an I/O wait for more than eighteen minutes. Mail/calendar/server gates remain behind its target lock; screenshots, the real-server probes and the performance profile cannot run until the binary exists. No result is claimed for those pending checks.
Author
Owner

The real Mail robustness probe reached the Save to Files check and timed out waiting for the attachment row. All preceding API rejection, authentication, HTML and isolation assertions had passed; the failure was in the UI deep-link restoration.

In the merged #640 reader, attachment restoration existed only inside the cold fetchMessageDetail continuation. Cached message selection returned before that code, and thread routes did not fetch their attachment list. The fix restores ?attachment= from a shared route effect for both warm messages and threads through the existing openAttachments function. Closing the inspector does not reopen it until the requested route changes. It removes the old frame wait; data and input do not wait for motion. The existing probe's assertions and selectors are unchanged. Verification of this fix is pending the production rebuild.

The real Mail robustness probe reached the Save to Files check and timed out waiting for the attachment row. All preceding API rejection, authentication, HTML and isolation assertions had passed; the failure was in the UI deep-link restoration. In the merged #640 reader, attachment restoration existed only inside the cold `fetchMessageDetail` continuation. Cached message selection returned before that code, and thread routes did not fetch their attachment list. The fix restores `?attachment=` from a shared route effect for both warm messages and threads through the existing `openAttachments` function. Closing the inspector does not reopen it until the requested route changes. It removes the old frame wait; data and input do not wait for motion. The existing probe's assertions and selectors are unchanged. Verification of this fix is pending the production rebuild.
Author
Owner

Static audit evidence and updated owner requirement for DESIGN §45:

apps/web/src/lib/mail/MailView.svelte:537-540 renders a notice saying remote images are blocked and asks the User to press Load images. The owner rule dated 2026-10-02 requires privacy protection to work silently by default, with no warning banner or extra click in the normal flow. This newer rule supersedes the current ask-to-load acceptance in this issue.

Expected: protect remote image requests in the background, for example by proxying and prefetching them, and remove the notice and prompt. Regression idea: open HTML mail and confirm the browser does not request third-party image URLs directly and the message needs no extra click.

Static audit evidence and updated owner requirement for DESIGN §45: apps/web/src/lib/mail/MailView.svelte:537-540 renders a notice saying remote images are blocked and asks the User to press Load images. The owner rule dated 2026-10-02 requires privacy protection to work silently by default, with no warning banner or extra click in the normal flow. This newer rule supersedes the current ask-to-load acceptance in this issue. Expected: protect remote image requests in the background, for example by proxying and prefetching them, and remove the notice and prompt. Regression idea: open HTML mail and confirm the browser does not request third-party image URLs directly and the message needs no extra click.
Author
Owner

Copy audit evidence (owner privacy rule, 2026-10-02)

apps/web/src/lib/mail/MailView.svelte:173 exposes Block images from this sender / Load images from this sender. :539 shows Remote images are blocked. and asks the User to Load images.

Expected: fetch remote pictures through the server so the sender does not learn the User's address or when they open a message. Keep the default safe. Do not show a blocked notice or ask the User to load pictures in the normal reading flow. Use a plain setting only if the User needs to change this choice.

This owner rule supersedes the older DESIGN §45 and this issue's only after the User loads images wording. Keep the strict frame and the server image proxy.

Test idea: with a remote-image message, confirm that the reader shows no notice or action. Confirm that the browser makes no request to the sender and that the server fetch does not send cookies or a referrer.

Copy audit evidence (owner privacy rule, 2026-10-02) `apps/web/src/lib/mail/MailView.svelte:173` exposes `Block images from this sender` / `Load images from this sender`. `:539` shows `Remote images are blocked.` and asks the User to `Load images`. Expected: fetch remote pictures through the server so the sender does not learn the User's address or when they open a message. Keep the default safe. Do not show a blocked notice or ask the User to load pictures in the normal reading flow. Use a plain setting only if the User needs to change this choice. This owner rule supersedes the older DESIGN §45 and this issue's `only after the User loads images` wording. Keep the strict frame and the server image proxy. Test idea: with a remote-image message, confirm that the reader shows no notice or action. Confirm that the browser makes no request to the sender and that the server fetch does not send cookies or a referrer.
Author
Owner

More Mail copy evidence in the connected browser tools

apps/web/src/lib/webmcp/tools.ts:239,241 exposes Set Mail remote images, Allow or block remote images for a Mail sender after User confirmation., and confirmation labels Allow remote images from this sender? / Block remote images from this sender?.

The 2026-10-02 privacy rule says remote pictures should load through the server by default, without a warning or prompt. Apply that rule to the connected tools too. If the User needs a per-sender choice, use plain wording such as Show pictures from this sender and keep the safe default.

Test idea: read the tool list and confirmation copy from a connected browser client. Confirm that normal message reading does not ask the User to allow pictures and that the sender does not see the User's address or open time.

More Mail copy evidence in the connected browser tools `apps/web/src/lib/webmcp/tools.ts:239,241` exposes `Set Mail remote images`, `Allow or block remote images for a Mail sender after User confirmation.`, and confirmation labels `Allow remote images from this sender?` / `Block remote images from this sender?`. The 2026-10-02 privacy rule says remote pictures should load through the server by default, without a warning or prompt. Apply that rule to the connected tools too. If the User needs a per-sender choice, use plain wording such as `Show pictures from this sender` and keep the safe default. Test idea: read the tool list and confirmation copy from a connected browser client. Confirm that normal message reading does not ask the User to allow pictures and that the sender does not see the User's address or open time.
Author
Owner

Design-sync #864 corrected DESIGN §45 on job/design-sync, commit 01c9bf1f4.

Old docs/DESIGN.md:2173-2174 required consent before remote content loads. The owner comment on #726 dated 2026-10-02 supersedes that rule. DESIGN now records automatic server image-proxy loading, fetch at delivery or first sync, no cookies or Referer, size/type limits, silent tracking-pixel removal and the one default-on Settings switch. No reader Load images button or blocked notice. Sender layout and colours stay; dark adaptation is in ⋯.

#736's cache requirement stays: check current preference and access before showing retained content. Regression idea: no browser request to a remote image host; no upstream fetch on message open; tracking pixels stay absent; changing the setting affects retained content. No runtime claim or test run from this LIGHT documentation job.

Design-sync #864 corrected DESIGN §45 on `job/design-sync`, commit `01c9bf1f4`. Old docs/DESIGN.md:2173-2174 required consent before remote content loads. The owner comment on #726 dated 2026-10-02 supersedes that rule. DESIGN now records automatic server image-proxy loading, fetch at delivery or first sync, no cookies or Referer, size/type limits, silent tracking-pixel removal and the one default-on Settings switch. No reader Load images button or blocked notice. Sender layout and colours stay; dark adaptation is in ⋯. #736's cache requirement stays: check current preference and access before showing retained content. Regression idea: no browser request to a remote image host; no upstream fetch on message open; tracking pixels stay absent; changing the setting affects retained content. No runtime claim or test run from this LIGHT documentation job.
Author
Owner

mailhtml-726 final report — implementation committed; not merge-ready

Branch: job/mailhtml-726. Head: 76a79589e338d17dfe3f4c503b8a6e7c8b43e075. No push, deploy or issue closure. Stopped feature work at the five-hour limit.

Built

Parsed HTML/CSS presentation replaces the strip policy. The visible frame has an empty sandbox and a data-only CSP. Sender fonts and layout are kept. Sender dark CSS is used; optional dark adaptation has a stable message query. Parent-owned links have native link semantics and 44 px targets. Presentation tables have a low-specificity width-safe default.

Remote rasters are prefetched by a durable content worker. Every DNS answer and redirect passes the shared webcal address guard and is pinned. Requests carry no ambient proxy, cookies or Referer. Raster decoding, URL counts, dimensions, bytes, account cache size and concurrency are bounded. Declared pixels and known measurement hosts are omitted. Preparation uses revision checks and retained successor jobs. Cache reads require the User and Connected Account.

One default-on Settings → Mail switch, Load remote content, replaces reader prompts. It clears settled and pending body caches and supports rollback and Undo. Prepared-content polling does not wait for motion. Attachment deep links now restore for warm messages and threads through the existing inspector function.

Merged job/maillayouts first. Fetched and merged origin/dev once before final gates (32338690ade441bea45f62d1baa64b87c607cb08). Migration 0010 was free on that fetched dev. Generated OpenAPI, action registry and SDK from the integrated server binary.

Files

  • Cargo.lock
  • apps/web/e2e/mail-html-726.mjs
  • apps/web/src/lib/mail/MailMorphCard.svelte
  • apps/web/src/lib/mail/MailReaderContent.svelte
  • apps/web/src/lib/mail/MailView.svelte
  • apps/web/src/lib/mail/frame.test.ts
  • apps/web/src/lib/mail/frame.ts
  • apps/web/src/lib/mail/readerCache.test.ts
  • apps/web/src/lib/mail/readerCache.ts
  • apps/web/src/routes/settings/mail/MailSection.svelte
  • apps/web/src/routes/settings/mail/MailSection.svelte.test.ts
  • bench/mail-html-726.mjs
  • contracts/actions.json
  • contracts/openapi.json
  • crates/calternal-api/src/lib.rs
  • crates/calternal-api/src/public_address.rs
  • crates/plugins/calendar/src/feeds/subscriptions.rs
  • crates/plugins/mail/Cargo.toml
  • crates/plugins/mail/migrations/0010_faithful_html.sql
  • crates/plugins/mail/src/cache.rs
  • crates/plugins/mail/src/cache/store.rs
  • crates/plugins/mail/src/html.rs
  • crates/plugins/mail/src/lib.rs
  • crates/plugins/mail/src/remote.rs
  • crates/plugins/mail/src/routes.rs
  • crates/plugins/mail/src/sync.rs
  • docs/parity-matrix.md
  • packages/api-client/src/generated.ts
  • tests/adversarial/mail_api.mjs
  • tests/adversarial/mail_html_fixture.py
  • tests/adversarial/mail_html_newsletters.json
  • tests/adversarial/xuser_matrix.py

Gates — output quoted verbatim

cargo fmt --check: no output, exit 0.

API Clippy:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 10s

API tests:

test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.32s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Mail tests:

test result: ok. 58 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 12.06s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Calendar test results before stopping:

test result: ok. 83 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 21.20s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.16s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s

Mail Clippy:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 73m 00s

Calendar Clippy:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 7m 54s

Calendar doc-tests did not finish before the deadline. Server Clippy and tests did not run. The cold server build passed:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 79m 49s

Web check:

svelte-check found 0 errors and 0 warnings

The plain bun run test hit 18 five-second timeouts on the shared host. The reduced-worker run bun run test --maxWorkers=2 --testTimeout=30000 passed:

 Test Files  155 passed (155)
      Tests  1059 passed (1059)
   Duration  916.19s (transform 41%, environment 26%, import 16%, tests 11%, setup 6%)

The final frame regression run passed:

 Test Files  1 passed (1)
      Tests  3 passed (3)
   Duration  15.04s (environment 99%, transform 1%)

Final web build: exit 0. The sequential production HTML/client build identifiers matched. Concurrent check/build had earlier produced mismatched identifiers; those artifacts were rebuilt.

Real-server checks

Mail API probe: remote-content isolation, hostile IDs, cross-User message/thread/attachment isolation, safe attachment names and 24 parallel account/Inbox reads passed
Cross-User cached-image matrix: owner 200, other User 404, anonymous 401; foreign and missing profiles match.

The matrix used real installation sessions and the existing xuser probe. Image route unit coverage also checks missing scope and disabled Connected Accounts. The security reviewer found no confirmed critical issue. Its selector, declared-pixel, cache lifetime, disabled-account and MIME findings were fixed with regressions.

Visual evidence

Partial macOS phone evidence and profile. Six 390 px newsletter captures exist: three synthetic letters in light and dark. They are test-only rows on a real server and use production assets. No screenshot was committed.

The full evidence run failed to find a reader after changing message routes. Tablet, desktop and Settings captures are missing. The cause of the later reader failure is unresolved; the harness now uses full deep-link navigation for future runs. The orchestrator must review a complete screenshot set before merge.

Performance

Local shared host, debug server; no comparable Mail HTML entry exists in docs/perf/baseline.json. This is not a release baseline comparison. The last profile measured:

  • Normal read p50/p95: 12.38/19.58 ms.
  • Large read p50/p95: 322.78/1115.63 ms.
  • Burst of 24 image reads: 1850.01 ms.
  • CPU: 3440 ms, 43.54% over the measured run. RSS: 337068 KiB. Load: [47.33, 50.14, 60.95].
  • Largest fixture: 64473 HTML bytes and 2074938 PNG bytes.

UX gaps closed

Removed the blocked-image notice and consent buttons. Added the global preference with Undo and shared-cache invalidation. Kept sender appearance and parent-owned link targets. Fixed the shared-root dark flag, phone table clipping and cold-only attachment-link restoration. The original robustness probe verifies the attachment save action after that fix.

UX gaps left

The new external-link pointer/touch/keyboard checks, optional adaptation and Settings Undo checks did not complete in the browser. Their component/frame tests pass, but real-browser evidence remains required. Full tablet/desktop/Settings captures and the later message-route failure remain open. Internal newsletter fragment links are not implemented.

Known gaps

The existing 64 KiB provider body limit remains. SVG, remote fonts, unparsed/custom CSS and unsupported at-rules are omitted. Tracking detection is heuristic; simple declared pixels are removed before fetching, and actual one-pixel raster responses are rejected after receipt. Complex selector-based or disguised tracking is not proven absent. Failed or reclaimed remote images remain absent until later source preparation; there is no reader-side retry. Pending polling stops after a bounded minute. Upstream prefetch throughput has not been profiled.

The real-server binary predates the last cache/CSS hardening edits. Those edits pass current Mail tests and Clippy; a current-head server build and full server gates remain required.

Decisions

The visible frame remains opaque. A hidden inert sizing document has only allow-same-origin, no scripts, and the same data-only CSP, because the opaque frame cannot report natural height. Per-account derived images use a 64 MiB/10,000-row budget; oldest entries are reclaimed with headroom. Source messages are never removed. The legacy sender-rule endpoint remains for compatibility, but rendering uses the User preference. Dark adaptation is a per-message query. These choices need owner confirmation after the full evidence review.

Cleanup: cargo clean and web-output deletion were requested at the deadline. Build artifacts stay out of Git; review artifacts remain in artifacts/.

# mailhtml-726 final report — implementation committed; not merge-ready Branch: `job/mailhtml-726`. Head: `76a79589e338d17dfe3f4c503b8a6e7c8b43e075`. No push, deploy or issue closure. Stopped feature work at the five-hour limit. ## Built Parsed HTML/CSS presentation replaces the strip policy. The visible frame has an empty sandbox and a data-only CSP. Sender fonts and layout are kept. Sender dark CSS is used; optional dark adaptation has a stable message query. Parent-owned links have native link semantics and 44 px targets. Presentation tables have a low-specificity width-safe default. Remote rasters are prefetched by a durable content worker. Every DNS answer and redirect passes the shared webcal address guard and is pinned. Requests carry no ambient proxy, cookies or Referer. Raster decoding, URL counts, dimensions, bytes, account cache size and concurrency are bounded. Declared pixels and known measurement hosts are omitted. Preparation uses revision checks and retained successor jobs. Cache reads require the User and Connected Account. One default-on Settings → Mail switch, Load remote content, replaces reader prompts. It clears settled and pending body caches and supports rollback and Undo. Prepared-content polling does not wait for motion. Attachment deep links now restore for warm messages and threads through the existing inspector function. Merged `job/maillayouts` first. Fetched and merged `origin/dev` once before final gates (`32338690ade441bea45f62d1baa64b87c607cb08`). Migration 0010 was free on that fetched dev. Generated OpenAPI, action registry and SDK from the integrated server binary. ## Files - `Cargo.lock` - `apps/web/e2e/mail-html-726.mjs` - `apps/web/src/lib/mail/MailMorphCard.svelte` - `apps/web/src/lib/mail/MailReaderContent.svelte` - `apps/web/src/lib/mail/MailView.svelte` - `apps/web/src/lib/mail/frame.test.ts` - `apps/web/src/lib/mail/frame.ts` - `apps/web/src/lib/mail/readerCache.test.ts` - `apps/web/src/lib/mail/readerCache.ts` - `apps/web/src/routes/settings/mail/MailSection.svelte` - `apps/web/src/routes/settings/mail/MailSection.svelte.test.ts` - `bench/mail-html-726.mjs` - `contracts/actions.json` - `contracts/openapi.json` - `crates/calternal-api/src/lib.rs` - `crates/calternal-api/src/public_address.rs` - `crates/plugins/calendar/src/feeds/subscriptions.rs` - `crates/plugins/mail/Cargo.toml` - `crates/plugins/mail/migrations/0010_faithful_html.sql` - `crates/plugins/mail/src/cache.rs` - `crates/plugins/mail/src/cache/store.rs` - `crates/plugins/mail/src/html.rs` - `crates/plugins/mail/src/lib.rs` - `crates/plugins/mail/src/remote.rs` - `crates/plugins/mail/src/routes.rs` - `crates/plugins/mail/src/sync.rs` - `docs/parity-matrix.md` - `packages/api-client/src/generated.ts` - `tests/adversarial/mail_api.mjs` - `tests/adversarial/mail_html_fixture.py` - `tests/adversarial/mail_html_newsletters.json` - `tests/adversarial/xuser_matrix.py` ## Gates — output quoted verbatim `cargo fmt --check`: no output, exit 0. API Clippy: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 10s ``` API tests: ``` test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.32s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` Mail tests: ``` test result: ok. 58 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 12.06s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` Calendar test results before stopping: ``` test result: ok. 83 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 21.20s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.16s test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s ``` Mail Clippy: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 73m 00s ``` Calendar Clippy: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 7m 54s ``` Calendar doc-tests did not finish before the deadline. Server Clippy and tests did not run. The cold server build passed: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 79m 49s ``` Web check: ``` svelte-check found 0 errors and 0 warnings ``` The plain `bun run test` hit 18 five-second timeouts on the shared host. The reduced-worker run `bun run test --maxWorkers=2 --testTimeout=30000` passed: ``` Test Files 155 passed (155) Tests 1059 passed (1059) Duration 916.19s (transform 41%, environment 26%, import 16%, tests 11%, setup 6%) ``` The final frame regression run passed: ``` Test Files 1 passed (1) Tests 3 passed (3) Duration 15.04s (environment 99%, transform 1%) ``` Final web build: exit 0. The sequential production HTML/client build identifiers matched. Concurrent check/build had earlier produced mismatched identifiers; those artifacts were rebuilt. ## Real-server checks ``` Mail API probe: remote-content isolation, hostile IDs, cross-User message/thread/attachment isolation, safe attachment names and 24 parallel account/Inbox reads passed ``` ``` Cross-User cached-image matrix: owner 200, other User 404, anonymous 401; foreign and missing profiles match. ``` The matrix used real installation sessions and the existing xuser probe. Image route unit coverage also checks missing scope and disabled Connected Accounts. The security reviewer found no confirmed critical issue. Its selector, declared-pixel, cache lifetime, disabled-account and MIME findings were fixed with regressions. ## Visual evidence [Partial macOS phone evidence and profile](https://git.kayg.org/attachments/30a2cc26-f886-4cc9-818c-51ef9453b784). Six 390 px newsletter captures exist: three synthetic letters in light and dark. They are test-only rows on a real server and use production assets. No screenshot was committed. The full evidence run failed to find a reader after changing message routes. Tablet, desktop and Settings captures are missing. The cause of the later reader failure is unresolved; the harness now uses full deep-link navigation for future runs. The orchestrator must review a complete screenshot set before merge. ## Performance Local shared host, debug server; no comparable Mail HTML entry exists in `docs/perf/baseline.json`. This is not a release baseline comparison. The last profile measured: - Normal read p50/p95: 12.38/19.58 ms. - Large read p50/p95: 322.78/1115.63 ms. - Burst of 24 image reads: 1850.01 ms. - CPU: 3440 ms, 43.54% over the measured run. RSS: 337068 KiB. Load: [47.33, 50.14, 60.95]. - Largest fixture: 64473 HTML bytes and 2074938 PNG bytes. ## UX gaps closed Removed the blocked-image notice and consent buttons. Added the global preference with Undo and shared-cache invalidation. Kept sender appearance and parent-owned link targets. Fixed the shared-root dark flag, phone table clipping and cold-only attachment-link restoration. The original robustness probe verifies the attachment save action after that fix. ## UX gaps left The new external-link pointer/touch/keyboard checks, optional adaptation and Settings Undo checks did not complete in the browser. Their component/frame tests pass, but real-browser evidence remains required. Full tablet/desktop/Settings captures and the later message-route failure remain open. Internal newsletter fragment links are not implemented. ## Known gaps The existing 64 KiB provider body limit remains. SVG, remote fonts, unparsed/custom CSS and unsupported at-rules are omitted. Tracking detection is heuristic; simple declared pixels are removed before fetching, and actual one-pixel raster responses are rejected after receipt. Complex selector-based or disguised tracking is not proven absent. Failed or reclaimed remote images remain absent until later source preparation; there is no reader-side retry. Pending polling stops after a bounded minute. Upstream prefetch throughput has not been profiled. The real-server binary predates the last cache/CSS hardening edits. Those edits pass current Mail tests and Clippy; a current-head server build and full server gates remain required. ## Decisions The visible frame remains opaque. A hidden inert sizing document has only allow-same-origin, no scripts, and the same data-only CSP, because the opaque frame cannot report natural height. Per-account derived images use a 64 MiB/10,000-row budget; oldest entries are reclaimed with headroom. Source messages are never removed. The legacy sender-rule endpoint remains for compatibility, but rendering uses the User preference. Dark adaptation is a per-message query. These choices need owner confirmation after the full evidence review. Cleanup: cargo clean and web-output deletion were requested at the deadline. Build artifacts stay out of Git; review artifacts remain in artifacts/.
Author
Owner

Cleanup complete. Head remains 76a79589e338d17dfe3f4c503b8a6e7c8b43e075; git status --short is empty. Web production output and the temporary contract exporter were removed. Review artifacts remain in the worktree and the partial phone set is attached above.

cargo clean output:

     Removed 15933 files, 9.4GiB total

The final current-production-UI Mail robustness probe exited 0. Calendar unit/integration results are 83 + 1 + 3 passing tests; its doc-test process was stopped at the deadline. Server Clippy/tests did not run. The job remains not merge-ready for the gaps in the final report.

Cleanup complete. Head remains `76a79589e338d17dfe3f4c503b8a6e7c8b43e075`; `git status --short` is empty. Web production output and the temporary contract exporter were removed. Review artifacts remain in the worktree and the partial phone set is attached above. `cargo clean` output: ``` Removed 15933 files, 9.4GiB total ``` The final current-production-UI Mail robustness probe exited 0. Calendar unit/integration results are 83 + 1 + 3 passing tests; its doc-test process was stopped at the deadline. Server Clippy/tests did not run. The job remains not merge-ready for the gaps in the final report.
Author
Owner

Independent Mail HTML review

Review job: job/rev2-mailhtml-726, for #726 and #736.
Review branch base: c4faf184df726a9375ae0c13bdfb6018ac2cf57e.
Source: /home/kayg/Developer/calternal-wt/mailhtml-726, branch
job/mailhtml-726, head 76a79589e338d17dfe3f4c503b8a6e7c8b43e075.
Diff: git -C /home/kayg/Developer/calternal-wt/mailhtml-726 diff origin/dev...HEAD.
The diff base is 440e19dce23040ac8ebaae88f0469b6535b1afcb.
The local origin/dev ref is c4faf184df726a9375ae0c13bdfb6018ac2cf57e.
This review does not change the source branch.

Result

Do not treat the cached-content access work as complete. One P1 and three
P2 defects remain. No P3 defect is confirmed. These are source findings;
no build, test, server or browser was used. The review follows CLAUDE.md,
CONTEXT.md, DESIGN §45 and the latest #726 owner comment. The old reader
consent flow is superseded by automatic server fetching and one Settings
switch.

ID Priority Evidence in reviewed source Result and fix Issue
F1 P1 crates/plugins/mail/src/routes.rs:1432; crates/plugins/mail/src/remote.rs:413; crates/plugins/mail/src/cache/store.rs:1451 Message detail embeds cached images from a disabled Connected Account. The direct image route at routes.rs:1480 refuses them. Use the same current-access check for both reads. #736
F2 P2 apps/web/src/routes/settings/mail/MailSection.svelte:120; apps/web/src/lib/mail/MailView.svelte:576; apps/web/src/lib/mail/frame.ts:47 A cache clear affects one browser context. Another tab or an API preference change leaves warm bodies usable without checking current preference. Use current preference state, notifications and a revision fence. #736
F3 P2 apps/web/src/lib/webmcp/tools.ts:239; crates/plugins/mail/src/routes.rs:1749; crates/plugins/mail/src/routes.rs:1423 The connected tool still asks to allow or block one sender and receives success. Rendering ignores that sender grant. Update tools to the global preference and explicitly retire the obsolete operation. #726
F4 P2 crates/plugins/mail/src/html.rs:436; crates/plugins/mail/src/html.rs:519; apps/web/src/lib/mail/frame.ts:49 The sender body's class and ID are lost, but its CSS selectors remain. Body-specific layout and dark rules no longer match. Preserve sanitized root attributes or transform attributes and selectors together. #726

Full evidence, expected behavior, fixes and test ideas are in
audit-findings.md. Duplicate searches used all issue
states with remote, revoked, hydrate, sender and newsletter terms. #736 and
#726 already own these fixes. No new issue was created.

Controls checked in source

  • HTML uses html5ever and Ammonia. Event attributes and active tags are not
    allowed. Native href values become inert markers. The parent checks link
    destinations against the server list and permits HTTP, HTTPS and mailto.
  • CSS uses Lightning CSS. Unknown properties, custom properties and unparsed
    declarations are removed. Only style, media and supports rules survive.
    This removes import and font-face rules. Parsed URLs use the raster policy.
    Fixed positioning, animations and transitions are removed. CSS has byte
    and nesting limits. No executable-content or direct CSS network bypass was
    confirmed by this read-only trace.
  • The visible iframe has an empty sandbox. Its CSP denies scripts, forms,
    frames, connections and fonts. Images must use data URLs. A hidden sizing
    iframe has allow-same-origin, is inert, and uses the same CSP without a
    script grant (apps/web/src/lib/mail/frame.ts:63). This is an explicit
    deviation from #726's no-same-origin requirement. The implementation
    comment records it as a sizing decision. No escape is claimed.
  • remote.rs:166 checks all DNS answers, including address literals. It
    allows standard HTTP/HTTPS ports only. The shared address policy rejects
    private, loopback, link-local and special ranges. Each public redirect gets
    new checked and pinned addresses. There are at most three redirects.
  • remote.rs:229 disables ambient proxies and automatic redirects. A new
    client sends only an image Accept header; no cookie store or identity
    headers are installed. The deadline covers redirect transport. DNS has a
    separate two-second limit. Fetch concurrency is four.
  • Responses have a two-MiB cap, a raster type check, a full decode limit and
    dimension limits. Animated images become a still image. The decoder retains
    the fetch permit. Prefetch is capped at 32 URLs and eight seconds per
    message. Account caches have byte and row limits with reclamation.
  • Declared tiny images and known tracking hosts are excluded before fetching.
    Raster dimensions are checked after download too. That last check cannot
    undo a request to an unknown host whose image size is known only after
    download. No claim that all tracking services are detected is made.
  • Cache reads and writes include the immutable User ID and account ID.
    Browser cache keys include the User ID. No cross-User cache-key collision
    was found. F1 concerns the current account state, not a cross-User read.
  • The reader has no Load images button or blocked-image banner. Settings
    shows the one Load remote content switch with Undo. F3 leaves contradictory
    control text and behavior in connected tools.

Known gaps and decisions

The LIGHT rule prohibits builds, tests, runtime probes and screenshots.
No gate result, visual approval or security test pass is claimed. No package
was added or changed. No fetch, merge, push, deploy or clean was run. Existing
build output belongs to other jobs and was left intact.

The source also drops every font-face rule (html.rs:50) and denies fonts in
the frame CSP (frame.ts:48). This is safe, but #726 originally asked for
web fonts from allowed sources. There is no allowed-font source policy in
DESIGN §45. The owner must confirm this rendering gap or define that policy;
the reviewer does not propose enabling remote fonts by default.

Session-end cleanup remains separately tracked in #767. The reviewed cache
has clear/delete fences, but no lifecycle listener. MailView.svelte:735
still writes snapshots during teardown. User keys prevent a demonstrated
cross-User cache reuse; they do not prove session-end cleanup. Reuse #767's
fix rather than add another invalidation system.

Review decisions: group current-access and stale-preference findings under
#736. Group sender-control and rendering findings under #726. Keep F2 at P2:
it violates current preference, but embedded bytes and the data-only CSP do
not demonstrate a direct User-IP or message-open leak. Hidden same-origin
sizing and the font policy require owner confirmation. No product decision
was changed.

UX gaps closed and left

No product code was changed. No UX gap was closed by this review. F2, F3 and
F4 remain. Pointer, touch, keyboard, screen reader and macOS rendering have
not been verified. No screenshot-based visual judgment is made.

For the merge round

After the fixes, run the focused cache and frame tests:

cd apps/web && bunx vitest run src/lib/mail/readerCache.test.ts src/lib/mail/frame.test.ts src/routes/settings/mail/MailSection.svelte.test.ts --maxWorkers=2

Extend those tests first: current tests do not prove cross-tab preference
revocation or sender body-root fidelity. Run the affected Rust crate tests:

cargo test -p calternal-plugin-mail

Add a message-detail regression for disabled accounts; a direct image-route
check alone does not cover F1. The merge round must also run the production
newsletter evidence:

node apps/web/e2e/mail-html-726.mjs

It must prove no browser request goes to a sender, no upstream fetch starts
on message open, and every shown link works with pointer, touch and keyboard.
Capture phone 390 px, tablet 820 px and desktop 1440 px, light and dark, with
macOS platform rendering. Add two-tab revocation and body-class dark CSS to
the fixture. The current script also calls a local performance profile;
disable that call for this merge round under the current verification policy.
Run the merge round's existing authorization and robustness
matrices once after the source fixes. This review supplies no runtime result.

Gate output verbatim: none. Gates were not run under the LIGHT job rule.

Review head: b66b68f0aff34f461bae317ce639e019863b7947.
Files committed: audit-findings.md, review-mailhtml-726.md.
Evidence added to #736. F3 and F4 extend #726 in this comment.
No new issues, source changes, builds, tests, screenshots, pushes, deploys or merges.

# Independent Mail HTML review Review job: `job/rev2-mailhtml-726`, for #726 and #736. Review branch base: `c4faf184df726a9375ae0c13bdfb6018ac2cf57e`. Source: `/home/kayg/Developer/calternal-wt/mailhtml-726`, branch `job/mailhtml-726`, head `76a79589e338d17dfe3f4c503b8a6e7c8b43e075`. Diff: `git -C /home/kayg/Developer/calternal-wt/mailhtml-726 diff origin/dev...HEAD`. The diff base is `440e19dce23040ac8ebaae88f0469b6535b1afcb`. The local `origin/dev` ref is `c4faf184df726a9375ae0c13bdfb6018ac2cf57e`. This review does not change the source branch. ## Result Do not treat the cached-content access work as complete. One P1 and three P2 defects remain. No P3 defect is confirmed. These are source findings; no build, test, server or browser was used. The review follows CLAUDE.md, CONTEXT.md, DESIGN §45 and the latest #726 owner comment. The old reader consent flow is superseded by automatic server fetching and one Settings switch. | ID | Priority | Evidence in reviewed source | Result and fix | Issue | | --- | --- | --- | --- | --- | | F1 | P1 | `crates/plugins/mail/src/routes.rs:1432`; `crates/plugins/mail/src/remote.rs:413`; `crates/plugins/mail/src/cache/store.rs:1451` | Message detail embeds cached images from a disabled Connected Account. The direct image route at `routes.rs:1480` refuses them. Use the same current-access check for both reads. | #736 | | F2 | P2 | `apps/web/src/routes/settings/mail/MailSection.svelte:120`; `apps/web/src/lib/mail/MailView.svelte:576`; `apps/web/src/lib/mail/frame.ts:47` | A cache clear affects one browser context. Another tab or an API preference change leaves warm bodies usable without checking current preference. Use current preference state, notifications and a revision fence. | #736 | | F3 | P2 | `apps/web/src/lib/webmcp/tools.ts:239`; `crates/plugins/mail/src/routes.rs:1749`; `crates/plugins/mail/src/routes.rs:1423` | The connected tool still asks to allow or block one sender and receives success. Rendering ignores that sender grant. Update tools to the global preference and explicitly retire the obsolete operation. | #726 | | F4 | P2 | `crates/plugins/mail/src/html.rs:436`; `crates/plugins/mail/src/html.rs:519`; `apps/web/src/lib/mail/frame.ts:49` | The sender body's class and ID are lost, but its CSS selectors remain. Body-specific layout and dark rules no longer match. Preserve sanitized root attributes or transform attributes and selectors together. | #726 | Full evidence, expected behavior, fixes and test ideas are in [audit-findings.md](audit-findings.md). Duplicate searches used all issue states with remote, revoked, hydrate, sender and newsletter terms. #736 and #726 already own these fixes. No new issue was created. ## Controls checked in source - HTML uses html5ever and Ammonia. Event attributes and active tags are not allowed. Native href values become inert markers. The parent checks link destinations against the server list and permits HTTP, HTTPS and mailto. - CSS uses Lightning CSS. Unknown properties, custom properties and unparsed declarations are removed. Only style, media and supports rules survive. This removes import and font-face rules. Parsed URLs use the raster policy. Fixed positioning, animations and transitions are removed. CSS has byte and nesting limits. No executable-content or direct CSS network bypass was confirmed by this read-only trace. - The visible iframe has an empty sandbox. Its CSP denies scripts, forms, frames, connections and fonts. Images must use data URLs. A hidden sizing iframe has `allow-same-origin`, is inert, and uses the same CSP without a script grant (`apps/web/src/lib/mail/frame.ts:63`). This is an explicit deviation from #726's no-same-origin requirement. The implementation comment records it as a sizing decision. No escape is claimed. - `remote.rs:166` checks all DNS answers, including address literals. It allows standard HTTP/HTTPS ports only. The shared address policy rejects private, loopback, link-local and special ranges. Each public redirect gets new checked and pinned addresses. There are at most three redirects. - `remote.rs:229` disables ambient proxies and automatic redirects. A new client sends only an image Accept header; no cookie store or identity headers are installed. The deadline covers redirect transport. DNS has a separate two-second limit. Fetch concurrency is four. - Responses have a two-MiB cap, a raster type check, a full decode limit and dimension limits. Animated images become a still image. The decoder retains the fetch permit. Prefetch is capped at 32 URLs and eight seconds per message. Account caches have byte and row limits with reclamation. - Declared tiny images and known tracking hosts are excluded before fetching. Raster dimensions are checked after download too. That last check cannot undo a request to an unknown host whose image size is known only after download. No claim that all tracking services are detected is made. - Cache reads and writes include the immutable User ID and account ID. Browser cache keys include the User ID. No cross-User cache-key collision was found. F1 concerns the current account state, not a cross-User read. - The reader has no Load images button or blocked-image banner. Settings shows the one Load remote content switch with Undo. F3 leaves contradictory control text and behavior in connected tools. ## Known gaps and decisions The LIGHT rule prohibits builds, tests, runtime probes and screenshots. No gate result, visual approval or security test pass is claimed. No package was added or changed. No fetch, merge, push, deploy or clean was run. Existing build output belongs to other jobs and was left intact. The source also drops every font-face rule (`html.rs:50`) and denies fonts in the frame CSP (`frame.ts:48`). This is safe, but #726 originally asked for web fonts from allowed sources. There is no allowed-font source policy in DESIGN §45. The owner must confirm this rendering gap or define that policy; the reviewer does not propose enabling remote fonts by default. Session-end cleanup remains separately tracked in #767. The reviewed cache has clear/delete fences, but no lifecycle listener. `MailView.svelte:735` still writes snapshots during teardown. User keys prevent a demonstrated cross-User cache reuse; they do not prove session-end cleanup. Reuse #767's fix rather than add another invalidation system. Review decisions: group current-access and stale-preference findings under #736. Group sender-control and rendering findings under #726. Keep F2 at P2: it violates current preference, but embedded bytes and the data-only CSP do not demonstrate a direct User-IP or message-open leak. Hidden same-origin sizing and the font policy require owner confirmation. No product decision was changed. ## UX gaps closed and left No product code was changed. No UX gap was closed by this review. F2, F3 and F4 remain. Pointer, touch, keyboard, screen reader and macOS rendering have not been verified. No screenshot-based visual judgment is made. ## For the merge round After the fixes, run the focused cache and frame tests: ```text cd apps/web && bunx vitest run src/lib/mail/readerCache.test.ts src/lib/mail/frame.test.ts src/routes/settings/mail/MailSection.svelte.test.ts --maxWorkers=2 ``` Extend those tests first: current tests do not prove cross-tab preference revocation or sender body-root fidelity. Run the affected Rust crate tests: ```text cargo test -p calternal-plugin-mail ``` Add a message-detail regression for disabled accounts; a direct image-route check alone does not cover F1. The merge round must also run the production newsletter evidence: ```text node apps/web/e2e/mail-html-726.mjs ``` It must prove no browser request goes to a sender, no upstream fetch starts on message open, and every shown link works with pointer, touch and keyboard. Capture phone 390 px, tablet 820 px and desktop 1440 px, light and dark, with macOS platform rendering. Add two-tab revocation and body-class dark CSS to the fixture. The current script also calls a local performance profile; disable that call for this merge round under the current verification policy. Run the merge round's existing authorization and robustness matrices once after the source fixes. This review supplies no runtime result. Gate output verbatim: none. Gates were not run under the LIGHT job rule. Review head: `b66b68f0aff34f461bae317ce639e019863b7947`. Files committed: `audit-findings.md`, `review-mailhtml-726.md`. Evidence added to #736. F3 and F4 extend #726 in this comment. No new issues, source changes, builds, tests, screenshots, pushes, deploys or merges.
Author
Owner

Review fixes for job/mailhtml-726 (F1–F4, includes blocker #736)

Branch job/mailhtml-726, head b45b94fb0. Not pushed or merged.

ID Fix Regression test (fails on the old code)
F1 (P1, #736) Message detail and the direct image route use one access rule: remote::content_access / account_active and one remote::cached_image read (it joins on a.enabled=1). If a Connected Account is off, the reader shows its local text and no cached image bytes. remote_content_allowed still reports the User preference. routes::tests::disabled_account_message_detail_embeds_no_cached_images (fails when the access check is reverted)
F2 (P2, #736) New apps/web/src/lib/mail/remoteContent.ts keeps the current preference apart from retained bodies. A change made in Settings or by the in-page tool, and turning an account off or removing it, clears the body cache and sends the change to the User's other tabs (a per-User BroadcastChannel). A revision counter makes sure a read that started before the change cannot bring the old value back. A tab also checks again on focus, when it becomes visible, and in the background (at most every 15 s) when it shows a warm body. MailView re-fetches the open message, drops image bytes at once when the User turns the switch off, and never caches a stale body again. remoteContent.test.ts (two tabs on a real BroadcastChannel; a late read; a change made through the API elsewhere; account off; another User), plus MailSection.svelte.test.ts
F3 (P2, #726) The per-sender route POST /mail/messages/{id}/remote-content is removed, with its action, the CLI command, the server MCP remote_content action and the WebMCP tool. Migration 0011 drops mail_remote_content_senders. The tools now change the one switch (calternal_mail_set_remote_content, MCP set_remote_content, calternal mail remote-content [true|false]). PATCH /preferences accepts either field alone, so changing the switch no longer overwrites read marking. An empty request returns 400. routes test: the retired route does not return 2xx; preferences test: a switch-only change keeps read marking; tools.test.ts
F4 (P2, #726) The server puts the sender body's class, id, dir (ltr/rtl/auto) and lang (bounded) on a div[data-mail-body] root, and removes sender-made markers. The frame moves these attributes onto its real <body> in both the visible frame and the sizing frame, so body.x and #id rules, including sender dark rules, match again. Frame height now includes body padding that sender CSS adds. html::tests::body_root_attributes_survive_for_sender_selectors (fails when the attributes are dropped), frame.test.ts

Contracts are regenerated: openapi.json, actions.json, generated.ts, parity-matrix.md. docs/mcp.md, tests/adversarial/mail_api.mjs and xuser_matrix.py are updated (the snapshot now covers mail_user_preferences).

Shared raster transport: not on this branch

Mail still has its own fetch path in crates/plugins/mail/src/remote.rs (fetch, fetch_pinned, addresses, tracking_host). The shared crates/calternal-plugin/src/raster_transport.rs exists only on job/browserfix, which is not in dev. job/browserfix must merge first. Then rebase this branch and switch Mail's fetch to raster_transport::fetch_image, image_url and tracking_host. Keep Mail's 2 MiB cap, decode and still-image steps, and cache policy after the shared fetch, so there is one image proxy.

Gates (verbatim)

  • cargo fmt --check (mail, server, cli): clean
  • cargo clippy -p calternal-plugin-mail -p calternal-cli -p calternal-server --all-targets -- -D warnings: Finished \dev` profile [unoptimized + debuginfo] target(s) in 44m 23s`
  • cargo test -p calternal-plugin-mail: test result: ok. 60 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out
  • cargo test -p calternal-cli: test result: ok. 33 passed; 0 failed; 0 ignored
  • cargo test -p calternal-api: test result: ok. 15 passed; 0 failed / test result: ok. 9 passed; 0 failed
  • cargo test -p calternal-server --bin calternal-server -- mcp contract openapi: test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 98 filtered out
  • bun run check: COMPLETED 1998 FILES 0 ERRORS 0 WARNINGS 0 FILES_WITH_PROBLEMS
  • Focused Vitest (readerCache, frame, remoteContent, MailSection, webmcp tools): Test Files 5 passed (5) / Tests 29 passed (29)

Still to do in the merge round

  • The full cargo test -p calternal-server was not run; only the filtered set above ran.
  • Run node apps/web/e2e/mail-html-726.mjs and add the two-tab revocation and body-class dark-CSS fixtures. Capture screenshots at 390, 820 and 1440 px in light and dark.
  • Run the adversarial mail_api.mjs and xuser_matrix.py against a real server.
  • The owner must confirm two open points from the review: the hidden same-origin sizing frame and the font policy.
## Review fixes for job/mailhtml-726 (F1–F4, includes blocker #736) Branch `job/mailhtml-726`, head `b45b94fb0`. Not pushed or merged. | ID | Fix | Regression test (fails on the old code) | | --- | --- | --- | | F1 (P1, #736) | Message detail and the direct image route use one access rule: `remote::content_access` / `account_active` and one `remote::cached_image` read (it joins on `a.enabled=1`). If a Connected Account is off, the reader shows its local text and no cached image bytes. `remote_content_allowed` still reports the User preference. | `routes::tests::disabled_account_message_detail_embeds_no_cached_images` (fails when the access check is reverted) | | F2 (P2, #736) | New `apps/web/src/lib/mail/remoteContent.ts` keeps the current preference apart from retained bodies. A change made in Settings or by the in-page tool, and turning an account off or removing it, clears the body cache and sends the change to the User's other tabs (a per-User `BroadcastChannel`). A revision counter makes sure a read that started before the change cannot bring the old value back. A tab also checks again on focus, when it becomes visible, and in the background (at most every 15 s) when it shows a warm body. MailView re-fetches the open message, drops image bytes at once when the User turns the switch off, and never caches a stale body again. | `remoteContent.test.ts` (two tabs on a real BroadcastChannel; a late read; a change made through the API elsewhere; account off; another User), plus `MailSection.svelte.test.ts` | | F3 (P2, #726) | The per-sender route `POST /mail/messages/{id}/remote-content` is removed, with its action, the CLI command, the server MCP `remote_content` action and the WebMCP tool. Migration 0011 drops `mail_remote_content_senders`. The tools now change the one switch (`calternal_mail_set_remote_content`, MCP `set_remote_content`, `calternal mail remote-content [true\|false]`). `PATCH /preferences` accepts either field alone, so changing the switch no longer overwrites read marking. An empty request returns 400. | routes test: the retired route does not return 2xx; preferences test: a switch-only change keeps read marking; `tools.test.ts` | | F4 (P2, #726) | The server puts the sender body's `class`, `id`, `dir` (ltr/rtl/auto) and `lang` (bounded) on a `div[data-mail-body]` root, and removes sender-made markers. The frame moves these attributes onto its real `<body>` in both the visible frame and the sizing frame, so `body.x` and `#id` rules, including sender dark rules, match again. Frame height now includes body padding that sender CSS adds. | `html::tests::body_root_attributes_survive_for_sender_selectors` (fails when the attributes are dropped), `frame.test.ts` | Contracts are regenerated: `openapi.json`, `actions.json`, `generated.ts`, `parity-matrix.md`. `docs/mcp.md`, `tests/adversarial/mail_api.mjs` and `xuser_matrix.py` are updated (the snapshot now covers `mail_user_preferences`). ### Shared raster transport: not on this branch Mail still has its own fetch path in `crates/plugins/mail/src/remote.rs` (`fetch`, `fetch_pinned`, `addresses`, `tracking_host`). The shared `crates/calternal-plugin/src/raster_transport.rs` exists only on `job/browserfix`, which is not in `dev`. **`job/browserfix` must merge first.** Then rebase this branch and switch Mail's fetch to `raster_transport::fetch_image`, `image_url` and `tracking_host`. Keep Mail's 2 MiB cap, decode and still-image steps, and cache policy after the shared fetch, so there is one image proxy. ### Gates (verbatim) - `cargo fmt --check` (mail, server, cli): clean - `cargo clippy -p calternal-plugin-mail -p calternal-cli -p calternal-server --all-targets -- -D warnings`: `Finished \`dev\` profile [unoptimized + debuginfo] target(s) in 44m 23s` - `cargo test -p calternal-plugin-mail`: `test result: ok. 60 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out` - `cargo test -p calternal-cli`: `test result: ok. 33 passed; 0 failed; 0 ignored` - `cargo test -p calternal-api`: `test result: ok. 15 passed; 0 failed` / `test result: ok. 9 passed; 0 failed` - `cargo test -p calternal-server --bin calternal-server -- mcp contract openapi`: `test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 98 filtered out` - `bun run check`: `COMPLETED 1998 FILES 0 ERRORS 0 WARNINGS 0 FILES_WITH_PROBLEMS` - Focused Vitest (readerCache, frame, remoteContent, MailSection, webmcp tools): `Test Files 5 passed (5)` / `Tests 29 passed (29)` ### Still to do in the merge round - The full `cargo test -p calternal-server` was not run; only the filtered set above ran. - Run `node apps/web/e2e/mail-html-726.mjs` and add the two-tab revocation and body-class dark-CSS fixtures. Capture screenshots at 390, 820 and 1440 px in light and dark. - Run the adversarial `mail_api.mjs` and `xuser_matrix.py` against a real server. - The owner must confirm two open points from the review: the hidden same-origin sizing frame and the font policy.
Author
Owner

Owner decision (2026-10-03): show emails as sent, including sender web fonts — load them through the server proxy like images (same limits, no cookies/Referer, cached), not system fonts.

Owner decision (2026-10-03): show emails as sent, including sender web fonts — load them through the server proxy like images (same limits, no cookies/Referer, cached), not system fonts.
Author
Owner

Review-fix follow-up on job/mailhtml-726. Starting head b45b94fb07; merged origin/dev once at 48c94c9776. Merge head 188a06f95. Existing F1–F4 fixes are present. Audit found F2 still paints a warm body before asynchronous preference revalidation, and a stationary reader has no periodic preference check. Will fence warm rendering and check visible readers periodically. No dependencies changed.

Review-fix follow-up on job/mailhtml-726. Starting head b45b94fb07566763be89e544d31280d1aa95838f; merged origin/dev once at 48c94c9776660cee105be86c5a6ace90dd425367. Merge head 188a06f95. Existing F1–F4 fixes are present. Audit found F2 still paints a warm body before asynchronous preference revalidation, and a stationary reader has no periodic preference check. Will fence warm rendering and check visible readers periodically. No dependencies changed.
Author
Owner

F2 follow-up committed as 60548fac7: a warm body now waits for the shared current-preference request, including inside the 15-second background interval; a changed revision rejects it even when the preference value is unchanged (account invalidation). Visible stationary readers check at most every 15 seconds. Two new tests failed on the prior code and now pass. Focused Vitest: Test Files 5 passed (5); Tests 31 passed (31). Web check: svelte-check found 0 errors and 0 warnings. Mail clippy: Finished dev profile [unoptimized + debuginfo] target(s) in 1m 53s. Mail tests: test result: ok. 60 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 2.14s. No Rust behavior changed in this follow-up. Production evidence adds two-tab remote-cache revocation and real body-specific dark CSS; profile is opt-in. Full matrices are left for the merge round.

F2 follow-up committed as 60548fac7: a warm body now waits for the shared current-preference request, including inside the 15-second background interval; a changed revision rejects it even when the preference value is unchanged (account invalidation). Visible stationary readers check at most every 15 seconds. Two new tests failed on the prior code and now pass. Focused Vitest: Test Files 5 passed (5); Tests 31 passed (31). Web check: svelte-check found 0 errors and 0 warnings. Mail clippy: Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 53s. Mail tests: test result: ok. 60 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 2.14s. No Rust behavior changed in this follow-up. Production evidence adds two-tab remote-cache revocation and real body-specific dark CSS; profile is opt-in. Full matrices are left for the merge round.
Author
Owner

Resumed #726 on job/mailhtml-726 at 8e5777269f. Read CLAUDE.md, CONTEXT.md, DESIGN §45/§33 and all issue comments. Fetch origin/dev bd11bacb51 and browserfix 58ffb09648. Integrating both as requested; dev has Mail conflicts to resolve. Preserving existing uncommitted two-tab/body-selector evidence. Latest issue owner decision also requires sender web fonts through the server proxy; this was absent from the resumed implementation. Verification follows the latest job policy: focused tests and production screenshots here; full e2e/matrices and performance reserved for the merge round.

Resumed #726 on job/mailhtml-726 at 8e5777269fed5a12a4a0ca34573981612334a752. Read CLAUDE.md, CONTEXT.md, DESIGN §45/§33 and all issue comments. Fetch origin/dev bd11bacb5189d39176e7cd48d5e977f1694321c1 and browserfix 58ffb096486ff956ab0a8322dac7c76800042944. Integrating both as requested; dev has Mail conflicts to resolve. Preserving existing uncommitted two-tab/body-selector evidence. Latest issue owner decision also requires sender web fonts through the server proxy; this was absent from the resumed implementation. Verification follows the latest job policy: focused tests and production screenshots here; full e2e/matrices and performance reserved for the merge round.
Author
Owner

Integration finding: dev changed Settings imports while #726 retained the remote-content checkbox; the merged page lacked Checkbox. bun run check reported Cannot find name 'Checkbox'. Restored the shared component import. Browserfix cache keys now include the access/session epoch; the existing preference tests used literal pre-epoch keys and a storage mock without hintedUser. The tests now mint keys through mailCacheKey and keep all original assertions. Focused web result: Test Files 5 passed (5) / Tests 33 passed (33). Web result: svelte-check found 0 errors and 0 warnings. UI integration commit 2f8b0880b. Latest browserfix head 91f4a4d60 merged as requested.

Owner font decision is implemented with a minimal shared-transport addition (fetch_font/font_type), the same DNS/redirect/admission/privacy policy, 2 MiB compressed resources, 4 MiB declared expanded tables, four font sources per message, and the existing owned cache/quota. No new dependency. Migration 0012 broadens cache MIME types and requeues presentation. Inline sender images, CSS and fonts remain on an opaque data-only reader surface. Regression and production browser evidence pending current server build.

Integration finding: dev changed Settings imports while #726 retained the remote-content checkbox; the merged page lacked Checkbox. bun run check reported `Cannot find name 'Checkbox'`. Restored the shared component import. Browserfix cache keys now include the access/session epoch; the existing preference tests used literal pre-epoch keys and a storage mock without hintedUser. The tests now mint keys through mailCacheKey and keep all original assertions. Focused web result: `Test Files 5 passed (5)` / `Tests 33 passed (33)`. Web result: `svelte-check found 0 errors and 0 warnings`. UI integration commit 2f8b0880b. Latest browserfix head 91f4a4d60 merged as requested. Owner font decision is implemented with a minimal shared-transport addition (fetch_font/font_type), the same DNS/redirect/admission/privacy policy, 2 MiB compressed resources, 4 MiB declared expanded tables, four font sources per message, and the existing owned cache/quota. No new dependency. Migration 0012 broadens cache MIME types and requeues presentation. Inline sender images, CSS and fonts remain on an opaque data-only reader surface. Regression and production browser evidence pending current server build.
Author
Owner

Tablet evidence found that Mail publishes PageChrome.morphCard, but the app shell only mounts shortcut help in the expanded Tab Bar. /mail/m/<id> at 820 px therefore stays on the inbox list with no frame. Connecting the existing expansion slot and sidebar offset; the six-email browser test covers this regression. Phone sender-font decoding and link-target alignment now pass through the real content worker.

Server clippy passed. Server tests returned:

test result: FAILED. 163 passed; 2 failed; 5 ignored; 0 measured; 0 filtered out; finished in 21.10s

Both failures are the independent-review helper in crates/calternal-server/src/integrations_review.rs:88, which requires the last Mail migration version to equal 9; this branch adds versions 10–12 and returns 12. The SQL-equality assertion for migration 0009 was not reached. Per the owner rule, I am keeping the old expectation and will report this for the orchestrator to decide.

Tablet evidence found that Mail publishes `PageChrome.morphCard`, but the app shell only mounts shortcut help in the expanded Tab Bar. `/mail/m/<id>` at 820 px therefore stays on the inbox list with no frame. Connecting the existing expansion slot and sidebar offset; the six-email browser test covers this regression. Phone sender-font decoding and link-target alignment now pass through the real content worker. Server clippy passed. Server tests returned: ``` test result: FAILED. 163 passed; 2 failed; 5 ignored; 0 measured; 0 filtered out; finished in 21.10s ``` Both failures are the independent-review helper in `crates/calternal-server/src/integrations_review.rs:88`, which requires the last Mail migration version to equal 9; this branch adds versions 10–12 and returns 12. The SQL-equality assertion for migration 0009 was not reached. Per the owner rule, I am keeping the old expectation and will report this for the orchestrator to decide.
Author
Owner

READY FOR MERGE: no

Built

Branch job/mailhtml-726; head db9477547c29b74661687c8004145c19ab2c45b1. Integrated origin/dev at bd11bacb5189d39176e7cd48d5e977f1694321c1 and browserfix through 5774773d70e57894e4c29bfa58d286707c95eb27. No push, deploy, or merge into dev/main.

Mail keeps sender CSS, body selectors, tables, inline images and sender dark-mode rules inside an opaque, script-free reader. Images load automatically through the shared guarded transport and owned cache. The retired image-consent banner path is removed from Mail layout evidence. Sender web fonts now use the same server transport and cache, without a system-font rewrite. The sealed measurement frame refits height and parent-owned link targets when fonts finish decoding. The browser session fences retain browserfix's cache cancellation and prevent late private reader updates. Connected Account migration and new-Mail Events remain intact after conflict resolution.

The app shell now mounts Mail's existing morph reader in the expanded Tab Bar. Tablet deep links previously showed only the inbox list. It uses the existing shared motion and reduced-motion rules, plus CSS sidebar positioning.

Files

  • crates/plugins/mail/src/{html,remote,routes,sync,lib}.rs, src/cache/store.rs, migrations 0010–0012: preparation, owned hydration, preference policy and sender fonts.
  • crates/calternal-plugin/src/raster_transport.rs: shared pinned transport, bounded font containers and privacy regression.
  • apps/web/src/lib/mail/{MailView,MailReaderContent,MailMorphCard}.svelte, {frame,readerCache,remoteContent}.ts and focused tests: faithful reader, safe link controls, sizing and session/preference fences.
  • apps/web/src/routes/+layout.svelte, layout.css, settings/mail/MailSection.svelte: tablet capsule wiring and default-on preference with Undo.
  • apps/web/e2e/{mail-html-726,mail-layouts}.mjs, tests/adversarial/mail_html_fixture.py, mail_html_newsletters.json, mail_screenshot_fixture.py: six representative messages and production/live-server regressions.
  • bench/mail-html-726.mjs, docs/DESIGN.md, generated contracts and API client: font profile, owner decision and current contracts.
  • Browserfix changes are included by merge; its latest extra changes are browser evidence and a stable-Rust fixture comparison.

Gates (output verbatim)

cargo fmt --check: exit 0, no output. Cargo used line-tables-only, incremental disabled, four build jobs and the worktree TMPDIR. Clippy ran per crate with --all-targets -- -D warnings; tests ran per crate with --test-threads=4.

calternal-plugin-mail:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 6m 55s
test result: ok. 62 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 4.05s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

calternal-plugin:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 8m 05s
test result: ok. 31 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.59s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

calternal-api:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 4m 55s
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

calternal-plugin-calendar:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 4m 15s
test result: ok. 88 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 10.60s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.35s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.21s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

calternal-cli:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 56s
test result: ok. 33 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.77s
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.27s

calternal-server:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 58s
test result: FAILED. 163 passed; 2 failed; 5 ignored; 0 measured; 0 filtered out; finished in 21.10s

The focused 10 MiB HTML rejection regression, added after the Mail suite, returned:

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 65 filtered out; finished in 0.02s

Web bun run check:

svelte-check found 0 errors and 0 warnings

Focused Vitest output:

 Test Files  5 passed (5)
      Tests  33 passed (33)
 Test Files  1 passed (1)
      Tests  6 passed (6)
 Test Files  1 passed (1)
      Tests  24 passed (24)

Production web build passed. Focused live browser regression output:

Mail HTML #726: 42 macOS screenshots, cached-image ownership/anonymous/off-state checks, two-tab revocation, body dark selectors, sender-font decode, HTML containment, Undo and zero external resource requests passed.

Contract checks:

Action registry: 342 operations, 324 generated tools
Parity matrix: 342 API actions, 126 shortcuts, 2 static commands, 151 menu actions, 39 settings groups, 0 actions with adapter gaps

Gate blocker

Issue #1024 records the two server failures. Both stop at integrations_review.rs:88, which asserts the last Mail migration version is 9. This branch adds 10–12, so the value is 12. The migration-0009 SQL comparison and preservation checks are retained. Per the owner rule, the old expectation was not changed. The orchestrator must resolve this assertion before merge; READY is no.

Evidence

42 production screenshots: six messages × phone/tablet/desktop × light/dark, plus six Settings screenshots. All use macOS platform emulation. Fixtures include table newsletters, an order confirmation without financial data, a delivery update, inline images, dark-mode styles and a real bundled sender font. Test data stays in the disposable projection. Screenshot archives contain full originals; no review artifact is committed. Representative chrome/icon pairs were inspected in the screenshots; final visual review belongs to Claude.

UX gaps closed

  • Tablet stable message links now open the actual reader.
  • Sender fonts decode without external browser requests; height and link targets refit after loading.
  • Body class/id/language and sender dark selectors survive both frame documents.
  • Safe links work by pointer, touch, Space and Enter.
  • Remote-content off state revokes retained bytes in another open tab; Undo restores them.
  • HTML scripts, forms, CSS network sources, invalid remote-font ports, local-font probes and tracking pixels stay outside the visible document. Safe CSS remains.
  • No image-consent notice remains in the checked reader flow.

Known gaps / UX gaps left

  • Provider HTML ingestion still has its inherited 64 KiB body limit.
  • SVG, imported remote stylesheets, unknown/unparsed CSS, custom properties, SVG/EOT fonts and font collections are omitted. Tracking detection remains heuristic. Failed/reclaimed remote resources do not have a reader-triggered retry.
  • In-message fragment navigation is not implemented. Body-direct-child CSS selectors are not covered by this screenshot set; the presentation root can affect their match.
  • Real Safari/macOS interop and the complete cross-User/adversarial matrices belong to the merge round.

Decisions

  • Reuse the existing owned derived-content cache, account quota and shared DNS-pinned transport for sender fonts; do not add a second proxy or dependency.
  • Admit common WOFF/WOFF2/TTF/OTF containers: at most four remote font sources per message, 2 MiB encoded per font, 4 MiB declared expanded tables and 256 tables. Full font-table validation stays in the browser. Data-only font CSP permits the embedded bytes.
  • Reuse the existing expanded capsule slot for Mail's tablet morph reader; shortcut help takes precedence. Sidebar positioning uses CSS, without runtime alignment measurements.
  • Retain the explicitly approved sealed measurement frame with no scripts or network. Content/input does not wait for animation or font loading.

For the merge round

The current verification policy forbids full suites, release builds, full adversarial matrices, real Mac interop and performance measurements in this job. Run on the combined branch:

  • Resolve #1024, then cargo test -p calternal-server -- --test-threads=4: prove both migration orders retain SQL and account/cache preservation coverage.
  • cd apps/web && bun run test --maxWorkers=2: full web suite.
  • node apps/web/e2e/mail-layouts.mjs and node apps/web/e2e/browserfix-759.mjs: all Mail layouts, prepared automatic images and shared transport/session behavior.
  • MAIL_HTML_RUN_MATRIX=1 node apps/web/e2e/mail-html-726.mjs: cached content cross-User isolation plus this reader evidence.
  • tests/adversarial/run-split.sh: full authz, XUser and robustness matrices on the combined local server.
  • cargo build --release -p calternal-server: combined release build; staging/o2 and Apple-client checks remain merge-round work. Follow macdav-lab/apple-interop-2026-10-02.md under the Mac VM lock.

The bench/ profile now includes cached sender-font hydration. No performance numbers were collected: this issue is not a performance job, and the latest policy restricts measurements to performance issues on the locked perf VM. No comparable Mail HTML baseline exists in docs/perf/baseline.json.

Module comments were reread and updated for font hydration, transport limits, frame sizing, session fences and shell wiring. Working tree is clean. Cargo and web build output were removed; screenshots and gate logs remain under artifacts/mail-html-726/.

READY FOR MERGE: no ## Built Branch `job/mailhtml-726`; head `db9477547c29b74661687c8004145c19ab2c45b1`. Integrated `origin/dev` at `bd11bacb5189d39176e7cd48d5e977f1694321c1` and browserfix through `5774773d70e57894e4c29bfa58d286707c95eb27`. No push, deploy, or merge into dev/main. Mail keeps sender CSS, body selectors, tables, inline images and sender dark-mode rules inside an opaque, script-free reader. Images load automatically through the shared guarded transport and owned cache. The retired image-consent banner path is removed from Mail layout evidence. Sender web fonts now use the same server transport and cache, without a system-font rewrite. The sealed measurement frame refits height and parent-owned link targets when fonts finish decoding. The browser session fences retain browserfix's cache cancellation and prevent late private reader updates. Connected Account migration and new-Mail Events remain intact after conflict resolution. The app shell now mounts Mail's existing morph reader in the expanded Tab Bar. Tablet deep links previously showed only the inbox list. It uses the existing shared motion and reduced-motion rules, plus CSS sidebar positioning. ## Files - `crates/plugins/mail/src/{html,remote,routes,sync,lib}.rs`, `src/cache/store.rs`, migrations `0010`–`0012`: preparation, owned hydration, preference policy and sender fonts. - `crates/calternal-plugin/src/raster_transport.rs`: shared pinned transport, bounded font containers and privacy regression. - `apps/web/src/lib/mail/{MailView,MailReaderContent,MailMorphCard}.svelte`, `{frame,readerCache,remoteContent}.ts` and focused tests: faithful reader, safe link controls, sizing and session/preference fences. - `apps/web/src/routes/+layout.svelte`, `layout.css`, `settings/mail/MailSection.svelte`: tablet capsule wiring and default-on preference with Undo. - `apps/web/e2e/{mail-html-726,mail-layouts}.mjs`, `tests/adversarial/mail_html_fixture.py`, `mail_html_newsletters.json`, `mail_screenshot_fixture.py`: six representative messages and production/live-server regressions. - `bench/mail-html-726.mjs`, `docs/DESIGN.md`, generated contracts and API client: font profile, owner decision and current contracts. - Browserfix changes are included by merge; its latest extra changes are browser evidence and a stable-Rust fixture comparison. ## Gates (output verbatim) `cargo fmt --check`: exit 0, no output. Cargo used line-tables-only, incremental disabled, four build jobs and the worktree TMPDIR. Clippy ran per crate with `--all-targets -- -D warnings`; tests ran per crate with `--test-threads=4`. `calternal-plugin-mail`: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 6m 55s test result: ok. 62 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 4.05s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `calternal-plugin`: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 8m 05s test result: ok. 31 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.59s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `calternal-api`: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 4m 55s test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `calternal-plugin-calendar`: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 4m 15s test result: ok. 88 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 10.60s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.35s test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.21s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `calternal-cli`: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 56s test result: ok. 33 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.77s test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.27s ``` `calternal-server`: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 58s test result: FAILED. 163 passed; 2 failed; 5 ignored; 0 measured; 0 filtered out; finished in 21.10s ``` The focused 10 MiB HTML rejection regression, added after the Mail suite, returned: ``` test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 65 filtered out; finished in 0.02s ``` Web `bun run check`: ``` svelte-check found 0 errors and 0 warnings ``` Focused Vitest output: ``` Test Files 5 passed (5) Tests 33 passed (33) ``` ``` Test Files 1 passed (1) Tests 6 passed (6) ``` ``` Test Files 1 passed (1) Tests 24 passed (24) ``` Production web build passed. Focused live browser regression output: ``` Mail HTML #726: 42 macOS screenshots, cached-image ownership/anonymous/off-state checks, two-tab revocation, body dark selectors, sender-font decode, HTML containment, Undo and zero external resource requests passed. ``` Contract checks: ``` Action registry: 342 operations, 324 generated tools Parity matrix: 342 API actions, 126 shortcuts, 2 static commands, 151 menu actions, 39 settings groups, 0 actions with adapter gaps ``` ## Gate blocker [Issue #1024](https://git.kayg.org/kayg/calternal/issues/1024) records the two server failures. Both stop at `integrations_review.rs:88`, which asserts the last Mail migration version is 9. This branch adds 10–12, so the value is 12. The migration-0009 SQL comparison and preservation checks are retained. Per the owner rule, the old expectation was not changed. The orchestrator must resolve this assertion before merge; READY is no. ## Evidence 42 production screenshots: six messages × phone/tablet/desktop × light/dark, plus six Settings screenshots. All use macOS platform emulation. Fixtures include table newsletters, an order confirmation without financial data, a delivery update, inline images, dark-mode styles and a real bundled sender font. Test data stays in the disposable projection. Screenshot archives contain full originals; no review artifact is committed. Representative chrome/icon pairs were inspected in the screenshots; final visual review belongs to Claude. - [mailhtml-726-macos-390-light-dark.zip](https://git.kayg.org/attachments/397aa60f-47e7-4a76-b883-25c2ea7391c0) - [mailhtml-726-macos-820-light-dark.zip](https://git.kayg.org/attachments/609c524b-be63-4b77-81e2-9f56dea70fd9) - [mailhtml-726-macos-1440-light-dark.zip](https://git.kayg.org/attachments/df5c214a-3171-4202-a4a6-d0a754ee8843) ## UX gaps closed - Tablet stable message links now open the actual reader. - Sender fonts decode without external browser requests; height and link targets refit after loading. - Body class/id/language and sender dark selectors survive both frame documents. - Safe links work by pointer, touch, Space and Enter. - Remote-content off state revokes retained bytes in another open tab; Undo restores them. - HTML scripts, forms, CSS network sources, invalid remote-font ports, local-font probes and tracking pixels stay outside the visible document. Safe CSS remains. - No image-consent notice remains in the checked reader flow. ## Known gaps / UX gaps left - Provider HTML ingestion still has its inherited 64 KiB body limit. - SVG, imported remote stylesheets, unknown/unparsed CSS, custom properties, SVG/EOT fonts and font collections are omitted. Tracking detection remains heuristic. Failed/reclaimed remote resources do not have a reader-triggered retry. - In-message fragment navigation is not implemented. Body-direct-child CSS selectors are not covered by this screenshot set; the presentation root can affect their match. - Real Safari/macOS interop and the complete cross-User/adversarial matrices belong to the merge round. ## Decisions - Reuse the existing owned derived-content cache, account quota and shared DNS-pinned transport for sender fonts; do not add a second proxy or dependency. - Admit common WOFF/WOFF2/TTF/OTF containers: at most four remote font sources per message, 2 MiB encoded per font, 4 MiB declared expanded tables and 256 tables. Full font-table validation stays in the browser. Data-only font CSP permits the embedded bytes. - Reuse the existing expanded capsule slot for Mail's tablet morph reader; shortcut help takes precedence. Sidebar positioning uses CSS, without runtime alignment measurements. - Retain the explicitly approved sealed measurement frame with no scripts or network. Content/input does not wait for animation or font loading. ## For the merge round The current verification policy forbids full suites, release builds, full adversarial matrices, real Mac interop and performance measurements in this job. Run on the combined branch: - Resolve #1024, then `cargo test -p calternal-server -- --test-threads=4`: prove both migration orders retain SQL and account/cache preservation coverage. - `cd apps/web && bun run test --maxWorkers=2`: full web suite. - `node apps/web/e2e/mail-layouts.mjs` and `node apps/web/e2e/browserfix-759.mjs`: all Mail layouts, prepared automatic images and shared transport/session behavior. - `MAIL_HTML_RUN_MATRIX=1 node apps/web/e2e/mail-html-726.mjs`: cached content cross-User isolation plus this reader evidence. - `tests/adversarial/run-split.sh`: full authz, XUser and robustness matrices on the combined local server. - `cargo build --release -p calternal-server`: combined release build; staging/o2 and Apple-client checks remain merge-round work. Follow `macdav-lab/apple-interop-2026-10-02.md` under the Mac VM lock. The `bench/` profile now includes cached sender-font hydration. No performance numbers were collected: this issue is not a performance job, and the latest policy restricts measurements to performance issues on the locked perf VM. No comparable Mail HTML baseline exists in `docs/perf/baseline.json`. Module comments were reread and updated for font hydration, transport limits, frame sizing, session fences and shell wiring. Working tree is clean. Cargo and web build output were removed; screenshots and gate logs remain under `artifacts/mail-html-726/`.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#726
No description provided.