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
No Branch/Tag specified
dev
wip/hiderev-1153
wip/hide4-1153
wip/hide3-1153
wip/hide2-1153
wip/hide-1153
wip/editreg-1132
wip/editorrail3-1113
wip/editorrail2-1113
wip/editorrail-1113
wip/e2e-b2-1071
wip/e2e-b-1071
wip/draw4-1101
wip/draw3-1101
wip/draw2-1101
wip/draw-1101
wip/directory-1199-r
wip/directory-1199
wip/delete-1119
wip/collabrev-1197
wip/collabloss-1197
wip/cards2-1083
wip/cards-1083
wip/canvas-visual
wip/canvasvis2-976
wip/calhdr-1112
wip/calcards-1115
wip/browserfix
wip/blocks-1125
wip/allday-1107
wip/agenda-decks
wip/agenda-1086
wip/adv7c-1105
wip/txentry-1198
wip/trayicons2-1095
wip/trayicons-1095
wip/tagperf-1186
wip/sidebar3-1094
wip/rev2-webperf
wip/rev2-money-ident
job/adv-1202
wip/restyle-notes
wip/previewcard-1098
wip/palette2-1123
wip/palette-1093
wip/onboard2-1141
wip/onboard-1141.aborted-early
wip/onboard-1141
wip/nlpchip-1127
wip/morph-1104
wip/merge-round-7c5
wip/merge-round-7c4
job/notifloop-1194
wip/merge-round-7c3
wip/merge-round-7c2
wip/merge-round-7c
wip/mchrome-1084
wip/mailghost2-1094
wip/mailghost-1094
wip/kbpreview2-1118
wip/kbpreview-1118
wip/kanban-1092
wip/importhang-1121
job/collabloss-1197
job/onboard-1141
job/hide-1153
job/perf-1124
job/perf2-1124
job/tocrail-1191
job/restyle-settings
wip/restyle-settings
job/segmented-1200
wip/notifloop-1194
job/tagperf-1186
wip/segmented-1200
job/restyle-files
job/tagdnd-1187
job/merge30
job/cards-1179
wip/cards2-1179
wip/cards-1179
wip/tocrail-1191
wip/tagdnd-1187
wip/restyle-files
wip/perf-1124
wip/merge30j
job/restyle-notes
job/wizchoices-1140
wip/wizchoices-1140
wip/restyle-1190
job/moneyfmt-1180
job/txentry-1198
wip/moneyfmt2-1180
wip/moneyfmt-1180-r
wip/moneyfmt-1180
job/pillglass-1189
job/flags-1181
wip/flags-1181
job/restyle-1190
job/restyle-mailmoney
job/restyle-search
job/settingsreg-1195
job/wizard-1140
site/website
wip/wizardrev2-1140
wip/wizardrev-1140
wip/wizard5-1140
wip/wizard4-1140
wip/wizard3-1140
wip/wizard2-1140
wip/wizard-1140
wip/pillglass-1189
wip/settingsreg-1195
job/merge29
job/fu-1171
wip/merge29j
wip/fu-1171
job/fu-1166
job/directory-1199
job/proflog-1204
job/txresearch-1188
wip/fu-1166
job/merge28
job/search-1066
wip/search-1066
wip/merge28j
job/gateslot-1182
job/bulkimport-1157
job/mailnet-1160
wip/mailnetrev-1160
wip/mailnet-1160
wip/bulkrev-1157
wip/bulkimport-1157
job/startup-1161
wip/startup-1161
job/merge27
job/linkcards-1151
wip/linkcards3-1151
wip/linkcards2-1151
wip/linkcards-1151
job/traydate-1144
wip/traydate3-1144
wip/traydate2-1144
wip/traydate-1144
job/draw-1101
wip/merge27j
job/blockpill-1152
wip/blockpill3-1152
wip/blockpill2-1152
wip/blockpill-1152
job/minihover-1149
wip/minihover2-1149
wip/minihover-1149
job/merge25
wip/merge25-r
wip/merge25b
wip/merge25
job/inspector-1129
job/tags-1110
wip/inspector3-1129
wip/inspector2-1129
wip/inspector-1129
wip/tagsrev-1110
wip/tags2-1110
wip/tags-1110
job/dates-1148
wip/datesrev-1148
wip/dates2-1148
wip/dates-1148
job/licence-1145
wip/licence2-1145
wip/licence-1145
job/selfhost-1156
job/merge23
wip/merge23
job/tagfilter-1109
wip/tagfilter2-1109
wip/tagfilter-1109
job/kbd-1134
wip/kbd2-1134
wip/kbd-1134
job/palfoot-1137
wip/selfhost-1156
wip/palfoot2-1137
wip/palfoot-1137
job/toggle-1158
wip/toggle-1158
job/kbpreview-1118
job/docratchet-1155
job/perflint-1133
job/devtests-1159
wip/docratchet-1155
wip/devtests-1159
job/segv-1136
wip/toast-1142
wip/segv-1136
job/toast-1142
job/blockreload-1147
wip/blockreload-1147
job/font-1150
wip/font-1150
job/importui-1120
job/minimonth-1149
wip/importui-1120
wip/minimonth-1149
job/depcheck-1146
wip/perflint-1133
wip/depcheck-1146
job/calcards-1115
job/blocks-1125
job/plus-1128
job/shift-1138
wip/plus2-1128
wip/plus-1128
wip/shift-1138
job/moneyfid-1130
job/editorrail-1113
wip/moneyrev-1130
wip/moneyfid-1130
job/noext-851
wip/noext-851
wip/noext3-851
wip/noext2-851
job/week-1135
wip/week-1135
job/editreg-1132
job/smoke-1122
wip/smoke-1122
job/docs-1143
job/palette2-1123
job/calhdr-1112
job/nlpchip-1127
job/mailghost-1094
job/reconnect-1131
wip/reconnect-1131
job/trayicons-1095
job/delete-1119
job/importhang-1121
job/cards-1083
job/palette-1093
job/mchrome-1084
job/e2e-a-1071
job/canvas-visual
job/previewcard-1098
job/allday-1107
wip/e2e-a2-1071
wip/e2e-a-1071
job/e2e-b-1071
job/adv7c-1105
job/kanban-1092
job/agenda-1086
job/merge-round-7c
job/morph-1104
wip/surfaces-p2
job/merge-round-9
wip/merge-round-9
job/7cfix-small
wip/7cfix-small
job/mailui-1078
job/merge-round-8
wip/merge-round-8
wip/mailui-1078
job/mailround-1038
job/applemail-accept
wip/settitle-1068
wip/mailround2-1038
wip/mailround-1038
wip/e2e-7b
job/crash-1069
wip/crash-1069
job/searchlost-1066
wip/searchlost-1066
job/7b-reconcile
job/flake-1065
wip/flake-1065
wip/merge-round-7b7
wip/merge-round-7b6
wip/merge-round-7b5
wip/merge-round-7b4
wip/7b-reconcile
job/appupdate-1059
job/nfd-1044
wip/appupdate-1059
job/e2e-7b
job/loop-1062
wip/loop-1062
job/pdfprev-1045
job/invtoggle-1053
wip/pdfprev-1045
wip/nfd-1044
wip/invtoggle-1053
job/7bfix-e2e
job/mailstress-b
wip/7bfix-e2e
wip/mailstress-b
job/7bfix-adv
wip/7bfix-adv
job/mailstress-a
job/stack-1054
wip/stack-1054
wip/mailstress-a
job/mailstress-1038
wip/mailstress-1038
job/upload500-1051
wip/upload500-1051
job/share-1034
wip/share-1034
job/syncerr-1037
job/7bfix-photos
wip/7bfix-photos
job/paste-1036
job/setside-1039
wip/setside-1039
wip/paste-1036
job/lease-1042
wip/syncerr-1037
wip/lease-1042
job/7bfix-data
job/passkeybind-1043
wip/apprevoke-1041
job/invite-1035
wip/invite-1035
job/merge-round-7b2
wip/merge-round-7b2
job/mailproxy-486
job/apprevoke-1041
job/rebuild-1033
job/pillborder-1029
wip/pillborder-1029
wip/mailproxy-486
wip/applemail-486
job/headless-998
wip/headless-998
job/groups-1028
wip/groups-1028
job/rebuildwarn-1016
wip/rebuildwarn-1016
job/startup-1011
wip/startup-1011
job/monthpill-1009
job/bgthumb-1025
job/sharetitle-1012
wip/monthpill-1009
wip/bgthumb-1025
wip/sharetitle-1012
job/canvas-cards-977
wip/canvas-cards-977
job/canvas-pencil-978
job/canvas-sketch-990
wip/canvas-sketch-990
wip/canvas-pencil-978
job/canvas-files-989
wip/canvas-files-989
job/canvas-collab-991
wip/canvas-collab-991
job/weekscroll-1018
wip/weekscroll-1018
wip/canvas-core-976
job/canvas-core-976
job/round-drag
wip/round-drag
job/round-settings
job/browserfix
wip/oapi-974
job/oapi-974
job/hist2-integrate
job/mailhtml-726
wip/mailhtml-726
wip/hist2-integrate
job/moneyfu-984
job/drag-1015
wip/drag-1015
job/rename-1017
wip/rename-1017
job/hist2-api
wip/hist2-api
job/oneacct-1014
wip/oneacct-1014
wip/moneyfu-984
job/hist2-bench
job/hist2-restore
wip/hist2-bench
job/hist2-write
job/hotfix-724
wip/hotfix-724
wip/hist2-write
wip/hist2-restore
job/hist2-store
job/hist2-ui
wip/hist2-ui
wip/hist2-store
job/searchstarve-965
job/shutdown-963
wip/shutdown-963
wip/pubedit-981
job/pubedit-981
job/analytics-973
wip/searchstarve-965
job/authflash-850
job/weeklane-969
job/pvtitle-1004
job/hist-975
wip/authflash-850
job/voicepill-617
wip/pvtitle-1004
job/headring-1003
wip/weeklane-969
wip/voicepill-617
wip/headring-1003
wip/analytics-973
job/agentscope-980
wip/thumbsandbox-988
job/thumbsandbox-988
wip/hist-975
job/links-856
wip/links-856
job/davetag-966
wip/davetag-966
job/filesstorm-1000
job/hoverpad-725
wip/filesstorm-1000
job/ffmpegblas-993
job/merge-round-7a
wip/hoverpad-725
wip/ffmpegblas-993
job/nowdot-1002
wip/verify-7a
job/noteid-857
wip/nowdot-1002
wip/noteid-857
wip/merge-round-7a
wip/agentscope-980
job/imapedge
job/a11yfix2
wip/imapedge-941
wip/imapedge
wip/a11yfix2
job/notetask-986
job/logheading
wip/logheading-998
job/textthumb-652
job/photolive-987
wip/photolive-987
job/davactive-983
job/savefix-985
job/tabicons-607
wip/davactive-983
wip/tabicons-607
wip/notetask-986
wip/savefix-985
job/dirid-627
job/buildspeed-1007
wip/dirid-627
job/agenda-decks
job/perfguards-impl
job/undo-a11y
wip/undo-a11y
job/mailperf
job/wal-824
wip/settings-50
job/settings-50
job/notesfilter-606
wip/notesfilter-606
job/surfaces-p2
wip/wal-824
job/maillayouts
wip/mailperf
wip/maillayouts
job/taskmeta-659
job/money-ident
wip/money-ident
wip/taskmeta-659
job/errstates
wip/perfguards-impl
job/headings-881
wip/headings-881
wip/errstates
job/voice-619
job/gaps-827
job/notesperf
wip/notesperf
wip/voice-619
job/hddsql-549
job/perf-stream-668
wip/perf-stream-668
wip/deeplinks-fix
job/deeplinks-fix
job/authfix
job/docsfix-rust
wip/docsfix-rust
job/webperf
job/docsfix-web
job/datafix2
job/webdav-lock-476
job/copyfix
wip/copyfix
wip/webperf
job/focus-658
wip/protofix
job/mediafix
job/protofix
wip/mediafix
job/agentfix
job/hhmm-724
wip/agentfix
job/undo-722
job/reuse
wip/webdav-lock-476
wip/reuse
job/scopefix
job/datafix
wip/hhmm-724
wip/undo-722
job/surfaces-p1
wip/hddsql-549
job/voicememos-618
wip/datafix2
wip/surfaces-p1
job/fix-940
wip/fix-940
job/blaze-surfaces
wip/datafix
wip/blaze-surfaces
job/taskday-655
job/linknav-639
wip/linknav-639
wip/gaps-827
job/isolation-707
job/audiophotos-720
wip/audiophotos-720
job/advfind-664
wip/voicememos-618
wip/taskday-655
wip/isolation-707
wip/advfind-664
wip/scopefix
wip/focus-658
job/testgaps
wip/testgaps
job/overscroll-718
wip/authfix
job/deps
wip/overscroll-718
job/rev2-agentfix
job/rev2-money-ident
job/rev2-mailperf
wip/deps
job/hardening-728
wip/hardening-728
job/searchgen-832
wip/searchgen-832
job/photopw-849
job/mailsql-825
wip/photopw-849
job/sharefix
wip/sharefix
job/rev2-mailhtml-726
job/rev2-perfguards
job/copyval-723
job/lightglass-r2
wip/lightglass-r2
wip/docsfix-web
job/copy-audit
job/macinterop-staging-r2
job/design-sync
job/rev2-taskmeta-659
job/rev2-webperf
job/docs-audit
job/rev2-advfind-664
job/rev2-mailproxy-486
job/states-audit
job/rev2-datafix
job/design-drift
job/test-gaps
job/rev2-voicememos-618
job/rev2-mediafix
job/rev2-deps
job/rev2-datafix2
job/licence-audit
job/issue-hygiene
job/rev2-protofix
job/rev2-voice-619
job/rev2-isolation-707
job/rev2-surfaces-p1
job/deeplink-audit2
job/rev2-audiophotos-720
wip/test-gaps
job/rev2-overscroll-718
job/rev2-undo-722
wip/states-audit
job/rev2-dropmd-719
job/rev2-linknav-639
job/merge-7b-plan
wip/merge-7b-plan
job/rev2-taskday-655
wip/mailsql-825
job/rev2-webdav-lock-476
job/rev2-browserfix
wip/design-drift
job/rev2-hddsql-549
wip/deeplink-audit2
job/rev2-scopefix
job/rev2-authfix
job/rev2-hardening-728
job/rev2-wal-824
job/rev2-sharefix
job/calsidebar-638
job/chrome-audit
job/ioperf
wip/ioperf
wip/chrome-audit
wip/calsidebar-638
job/dropmd-719
wip/dropmd-719
job/ocr-build
wip/ocr-build
job/blaze-settings
wip/copyval-723
job/toastring-721
wip/toastring-721
job/deployfix-732
wip/deployfix-732
wip/blaze-settings
job/money-import-recheck
job/rev-a11y
job/perf-arch-db
job/rev-7b-data
wip/textthumb-652
wip/perf-arch-db
job/sec-protocols
job/sidehdr-660
job/rev-7b-security
job/research-surfaces
job/rev-design-gaps
job/rev-mcp-api
wip/sidehdr-660
job/perf-arch-memory
wip/sec-protocols
job/perf-arch-bundle
job/snapedge-714
wip/rev-mcp-api
job/sec-supplychain
wip/research-surfaces
job/perf-arch-sync
job/rev-consistency
job/perf-arch-server
wip/perf-arch-server
wip/perf-arch-memory
job/perf-arch-io
job/perf-arch-client
job/sec-fs
job/sec-mcp-scopes
job/sec-sharing
job/perf-guards
job/sec-browser
job/sec-admin-deploy
job/sec-auth
wip/snapedge-714
job/bgpicker-717
wip/perf-arch-bundle
wip/money-import-recheck
job/advsetup-654
wip/bgpicker-717
wip/advsetup-654
job/burst-709
job/kbdcaps-710
job/app-pw-chooser
wip/burst-709
wip/app-pw-chooser
job/imaptest-625
wip/kbdcaps-710
job/fix-499
wip/fix-499
job/perf-mut-667
job/calimg-589
job/perf-snap-666
wip/calimg-589
wip/perf-snap-666
wip/perf-mut-667
job/perf-cache-665
wip/perf-cache-665
job/voicefiles-620
wip/voicefiles-620
job/admin-burst-705
wip/admin-burst-705
job/voicememos-review
wip/voicememos-review
wip/ryw-653
job/ryw-653
job/writeonopen-661
job/instant-663
wip/writeonopen-661
job/money-import-review
wip/money-import-review
wip/importjs-610
review/integrations-407-round6
wip/integrations-review
job/dragghost-612
wip/dragghost-612
job/integrations
wip/integrations
job/decider-656
job/merge-round-6
job/perf-rerun
wip/merge-round-6
job/integrations-review-round5
job/selalign-576
wip/selalign-576
job/mcp-events-491
job/files-631
job/cal-e2e-569
wip/cal-e2e-569
job/reload-423
wip/reload-423
wip/mcp-events-491
wip/files-631
job/notesbridge-644
wip/notesbridge-644
job/editor-series
job/calcard-series
wip/calcard-series
job/mcp-events-review-491
wip/mcp-events-review
wip/editor-series
job/quirks-546
job/integrations-recheck
job/tocrail-636
wip/tocrail-636
wip/quirks-546
wip/reminders-643
job/reminders-643
wip/davscale-573
job/davscale-573
job/integrations-review
wip/ocr-eval-584
job/ocr-eval-584
job/esc-537
wip/esc-537
job/toastname-586
wip/toastname-586
job/submenu-579
wip/submenu-579
job/tasks-mode
wip/tasks-mode
job/agentdocs-630
job/dupwrite-634
wip/agentdocs-630
wip/dupwrite-634
job/lightglass-588
wip/lightglass-588
job/tabswitch-549
job/ghosttask-623
wip/ghosttask-623
job/toaststack-616
job/weekstate-609
job/mailsync-613
wip/mailsync-613
wip/weekstate-609
job/maildup-626
wip/tabswitch-549
wip/maildup-626
wip/toaststack-616
job/motion-611
wip/motion-611
job/tlstest-601
wip/tlstest-601
job/perf-495
job/floating-sheet
wip/floating-sheet
job/remdup-585
wip/remdup-585
job/fix-502
wip/fix-502
job/attachplay-622
job/perf-batch
wip/perf-batch-563
wip/perf-495
hotfix/mail-sync-diag
job/mail-m3
wip/mail-m3
job/attach-poof-603
job/calhover-608
job/editorbar-604
job/mentions-605
job/merge-round-4
job/allday-514
wip/merge-round-4
wip/allday-514
job/merge-round-4a
wip/merge-round-4a
job/sharestack-580
job/fix-501
wip/sharestack-580
wip/fix-501
job/perf-batch-563
job/apw-cache-review
wip/apw-cache-review
job/probe-520
wip/probe-520
job/mac-393
wip/mac-393
job/header-571
job/flake-513
wip/flake-513
job/docs-thumb-547
wip/header-571
job/webcal-572
wip/webcal-572
wip/shortcuts-542
job/shortcuts-542
wip/docs-thumb-547
job/caldav-stress
wip/caldav-stress
wip/sweep-478
job/apw-cache-512
wip/apw-cache-512
job/money-empty-540
wip/restart-505
wip/money-empty-540
wip/fix-510
job/restart-505
job/fix-503
job/perf-496
wip/perf-496
job/fix-498
wip/fix-498
job/info-inspector-465
wip/info-inspector-465
job/fix-510
job/fix-507
wip/fix-507
wip/fix-503
job/fix-493
job/money-kinds
wip/money-kinds
job/hygiene-548
job/merge-round-3
wip/fix-493
job/drag-snap-536
wip/merge-round-3
wip/merge-round-0930
wip/drag-snap-536
job/align-538
wip/align-538
job/bg-flash
wip/bg-flash
job/money-import
job/search-count-544
wip/search-count-544
wip/money-import
job/settings-key-541
wip/settings-key-541
job/toast-539
job/preview-421
wip/preview-421
wip/toast-539
job/tasks-500-531
job/title-plain-526
wip/title-plain-526
wip/tasks-500-531
job/notes-bridge
wip/parity-484
job/parity-484
job/files-slow
job/crash-525
wip/notes-bridge
wip/files-slow
wip/crash-525
job/kbd-motion-527
wip/bg-422
job/analytics-504
wip/analytics-504
wip/kbd-motion-527
job/upload-pill-523
wip/upload-pill-523
wip/tray-order
job/tray-order
wip/overflow-mid
wip/merge-round-2
job/perf-494
wip/perf-494
wip/mcp-fast-492
wip/motion-477
wip/asr-ab-489
wip/theme-variants-506
wip/overflow-511
wip/week-header-508
wip/attach-427
job/dav-delete-471
job/iso-435
wip/iso-435
wip/files-sel-keys
wip/dav-delete-471
job/align-253
job/siwc-490
wip/siwc-490
job/money-kinds-review
wip/align-253
wip/money-kinds-review
job/small-bugs-3
wip/overlay-title-487
wip/multiget-500
wip/hidden-420
wip/webcal-ui
wip/webcal-431
job/perf-367
job/location
wip/small-bugs-3
wip/location
wip/perf-367
wip/admin-deny-483
job/tag-unicode-473
wip/tag-unicode-473
job/blur-436
wip/photos-470
wip/blur-436
wip/small-bugs-4
wip/hunt-20260930
wip/settings-hdr-482
wip/chips-416
job/dedup-375
wip/dedup-375
job/doc-stack
wip/doc-stack
job/tokens-literals
wip/tokens-literals
job/jobs-leftovers
wip/send-fast
wip/paste-467
wip/money-numbers
job/money-plugin
wip/money-plugin
job/break-dav
wip/merge-batch
wip/crossday-469
wip/mac-verify
wip/mail-m2
wip/break-dav
wip/money-review2
job/money-md
job/modes-424
wip/money-md
wip/jobs-leftovers
job/agenda-413
wip/agenda-413
wip/modes-424
job/recog-417
wip/recog-417
wip/bounce-425
wip/ab-384-luna
job/webdav-perf
wip/webdav-perf
job/toast-ring
wip/toast-ring
job/money-review
wip/money-review
wip/micro-motion
wip/settings-card
wip/minical
job/notes-imap-428
job/least-priv
wip/ui-small-2
wip/flaky-426
wip/drag-end-418
job/jank
wip/jank
wip/least-priv
wip/docs-site
job/agenda
job/sec-batch
wip/sec-batch
wip/per-user-index
job/area-calendars
wip/area-calendars
job/parity
wip/parity
job/documents-research
wip/documents-research
job/test-infra
job/reminders-sync
wip/small-bugs-2
wip/reminders-sync
wip/gestures
job/google-oauth
wip/tags-merge
wip/tags
job/e2e-theme
wip/e2e-theme
job/icon-align
wip/test-infra
wip/select-align
wip/editor-385
job/voice
wip/webdav
job/webdav
job/app-pw-ui
job/editor-integrity
wip/editor-integrity
wip/voice
wip/quota
wip/cal-followups
wip/icon-align
job/composer-scale
wip/composer-scale
job/jobs-page
wip/jobs-page
job/hig-type
wip/hig-type
wip/app-pw-ui
job/motion-spring
job/mcp
wip/motion-spring
wip/mcp
job/small-bugs
wip/push-hosts
job/profile-sign
wip/touch-369
wip/profile-sign
job/mobile-focus
wip/mobile-focus
wip/ui-polish-354
wip/small-bugs
wip/dup-task
job/toast-polish
job/app-pw-scopes
wip/toast-polish
wip/app-pw-scopes
wip/cli-agent
wip/selection-pills
job/preview-attach
wip/preview-attach
job/dav-proppatch
wip/dav-proppatch
wip/cal-switcher
job/atomic-race
wip/atomic-race
job/photos-shared
wip/photos-shared
wip/cal-grid
wip/note-rewrite
wip/search-rebuild
job/mail-m1
job/paperless-import
wip/paperless-import
wip/mail-m1
wip/hidden-activity
wip/search-d
wip/pricing-research
wip/cursors
wip/auto-scheme
job/single-pills
wip/single-pills
wip/xuser-matrix
wip/money-format
wip/app-pw-setup
wip/purge-dos
wip/vault-health
wip/caldav-apple
wip/xuser-audit
wip/e2e-green
wip/tabbar
wip/adv-harness
wip/maple-mono
job/search-fix
wip/search-fix
wip/search-perf-c
job/adv-harness
wip/sidebar-headers
job/glass
wip/temp-index
job/polish
wip/polish
wip/file-protocols
wip/money-research
wip/glass
wip/voice-models
wip/collab-redo
job/voice-research
wip/hunt-20260928
wip/notes-actions-research
wip/search-pad
wip/search-perf
wip/search-sticky
wip/editor-undo
wip/chrome-rules
wip/motion
wip/appearance-research
wip/appearance
wip/audit-bugs
wip/cal-glass
wip/block-actions
wip/authz-order
wip/event-stripes
wip/chrome-sidebar
wip/auth-flaky
wip/robust-2
wip/gate-fix
wip/menu-blur
wip/import-calternaljs
wip/tray-fix
job/import-calternaljs
wip/index-order
wip/audit-fixes
wip/search-chevrons
research/mail
wip/phone-chrome
wip/dedup-break
wip/csp
wip/ui-audit
wip/select-toast
wip/perf
wip/flat-layout
wip/fonts
wip/event-tint
wip/sync-converge
wip/data-split
wip/glass-audit
wip/robustness
wip/sync-chaos
wip/search-thumbs
wip/fuzz
wip/menu-icons
wip/search-pill
wip/sync-changing
wip/heading-links
wip/date-formats
wip/a11y
wip/break-editor
wip/e2e-fix
wip/settings-sections
wip/sync-root-guard
wip/search-palette
wip/share-edit
job/toasts
wip/toasts
wip/cont-analytics
wip/authz-review
wip/popovers
wip/overlay-glass
wip/change-feed
wip/editor-modes
wip/composer-align
wip/cont-agenda
wip/agenda-merge
job/agent-conventions
wip/agent-conventions
wip/backend-misc
job/route-audit
wip/route-audit
wip/ui-batch
wip/heif-hardening
wip/grid-resize
wip/ask-page
wip/webmcp
job/deeplink-audit
wip/deeplinks
wip/shortcuts
wip/cont-tz-days
main
No results found.
Labels
Clear labels
No items
No labels
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
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
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.rskeeps "a small content vocabulary": it removes<style>, everystyleattribute, images, SVG, tables' presentation and more.apps/web/src/lib/mail/frame.tsthen 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)
<style>blocks, inlinestyle, tables,bgcolor/width/alignattributes, 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.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: dropexpression(),behavior,-moz-binding,@import, andposition: fixedoverlays that escape the frame. Remoteurl()in CSS is treated like remote images.cid:) images anddata:images under a size cap show at once.prefers-color-schemerules in the email.Tests
background:url(...),@font-faceremote), CSS escape attempts, huge tables, deeply nested tables, and 10 MB HTML.Owner direction (2026-10-02): the product must be normie-friendly; privacy features work silently in the background. This changes points 3 and 4:
Everything else in the issue stands.
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.The rev-consistency review (#427), on
origin/devatc4a61e8cf090170f35b1bed3350d9de20c83ecd5, found a privacy-flow conflict to include in #726's shared fix:apps/web/src/lib/mail/MailView.svelte:538-539shows “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.
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:
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.
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.
Browserfix found that the pinned
job/mailhtml-726tree 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/proxyroute 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.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.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.
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.
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.
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 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.
Source review found that the new Mail frame read
.dark, while the shared root publishes.is-darkanddata-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: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.
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
fetchMessageDetailcontinuation. 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 existingopenAttachmentsfunction. 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.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.
Copy audit evidence (owner privacy rule, 2026-10-02)
apps/web/src/lib/mail/MailView.svelte:173exposesBlock images from this sender/Load images from this sender.:539showsRemote images are blocked.and asks the User toLoad 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 imageswording. 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.
More Mail copy evidence in the connected browser tools
apps/web/src/lib/webmcp/tools.ts:239,241exposesSet Mail remote images,Allow or block remote images for a Mail sender after User confirmation., and confirmation labelsAllow 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 senderand 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.
Design-sync #864 corrected DESIGN §45 on
job/design-sync, commit01c9bf1f4.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.
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/maillayoutsfirst. Fetched and mergedorigin/devonce 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.lockapps/web/e2e/mail-html-726.mjsapps/web/src/lib/mail/MailMorphCard.svelteapps/web/src/lib/mail/MailReaderContent.svelteapps/web/src/lib/mail/MailView.svelteapps/web/src/lib/mail/frame.test.tsapps/web/src/lib/mail/frame.tsapps/web/src/lib/mail/readerCache.test.tsapps/web/src/lib/mail/readerCache.tsapps/web/src/routes/settings/mail/MailSection.svelteapps/web/src/routes/settings/mail/MailSection.svelte.test.tsbench/mail-html-726.mjscontracts/actions.jsoncontracts/openapi.jsoncrates/calternal-api/src/lib.rscrates/calternal-api/src/public_address.rscrates/plugins/calendar/src/feeds/subscriptions.rscrates/plugins/mail/Cargo.tomlcrates/plugins/mail/migrations/0010_faithful_html.sqlcrates/plugins/mail/src/cache.rscrates/plugins/mail/src/cache/store.rscrates/plugins/mail/src/html.rscrates/plugins/mail/src/lib.rscrates/plugins/mail/src/remote.rscrates/plugins/mail/src/routes.rscrates/plugins/mail/src/sync.rsdocs/parity-matrix.mdpackages/api-client/src/generated.tstests/adversarial/mail_api.mjstests/adversarial/mail_html_fixture.pytests/adversarial/mail_html_newsletters.jsontests/adversarial/xuser_matrix.pyGates — output quoted verbatim
cargo fmt --check: no output, exit 0.API Clippy:
API tests:
Mail tests:
Calendar test results before stopping:
Mail Clippy:
Calendar Clippy:
Calendar doc-tests did not finish before the deadline. Server Clippy and tests did not run. The cold server build passed:
Web check:
The plain
bun run testhit 18 five-second timeouts on the shared host. The reduced-worker runbun run test --maxWorkers=2 --testTimeout=30000passed:The final frame regression run passed:
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
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: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/.
Cleanup complete. Head remains
76a79589e338d17dfe3f4c503b8a6e7c8b43e075;git status --shortis 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 cleanoutput: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.
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, branchjob/mailhtml-726, head76a79589e338d17dfe3f4c503b8a6e7c8b43e075.Diff:
git -C /home/kayg/Developer/calternal-wt/mailhtml-726 diff origin/dev...HEAD.The diff base is
440e19dce23040ac8ebaae88f0469b6535b1afcb.The local
origin/devref isc4faf184df726a9375ae0c13bdfb6018ac2cf57e.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.
crates/plugins/mail/src/routes.rs:1432;crates/plugins/mail/src/remote.rs:413;crates/plugins/mail/src/cache/store.rs:1451routes.rs:1480refuses them. Use the same current-access check for both reads.apps/web/src/routes/settings/mail/MailSection.svelte:120;apps/web/src/lib/mail/MailView.svelte:576;apps/web/src/lib/mail/frame.ts:47apps/web/src/lib/webmcp/tools.ts:239;crates/plugins/mail/src/routes.rs:1749;crates/plugins/mail/src/routes.rs:1423crates/plugins/mail/src/html.rs:436;crates/plugins/mail/src/html.rs:519;apps/web/src/lib/mail/frame.ts:49Full 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
allowed. Native href values become inert markers. The parent checks link
destinations against the server list and permits HTTP, HTTPS and mailto.
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.
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 ascript grant (
apps/web/src/lib/mail/frame.ts:63). This is an explicitdeviation from #726's no-same-origin requirement. The implementation
comment records it as a sizing decision. No escape is claimed.
remote.rs:166checks all DNS answers, including address literals. Itallows 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:229disables ambient proxies and automatic redirects. A newclient 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.
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.
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.
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.
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 inthe frame CSP (
frame.ts:48). This is safe, but #726 originally asked forweb 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:735still 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:
Extend those tests first: current tests do not prove cross-tab preference
revocation or sender body-root fidelity. Run the affected Rust crate tests:
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:
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.
Review fixes for job/mailhtml-726 (F1–F4, includes blocker #736)
Branch
job/mailhtml-726, headb45b94fb0. Not pushed or merged.remote::content_access/account_activeand oneremote::cached_imageread (it joins ona.enabled=1). If a Connected Account is off, the reader shows its local text and no cached image bytes.remote_content_allowedstill reports the User preference.routes::tests::disabled_account_message_detail_embeds_no_cached_images(fails when the access check is reverted)apps/web/src/lib/mail/remoteContent.tskeeps 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-UserBroadcastChannel). 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), plusMailSection.svelte.test.tsPOST /mail/messages/{id}/remote-contentis removed, with its action, the CLI command, the server MCPremote_contentaction and the WebMCP tool. Migration 0011 dropsmail_remote_content_senders. The tools now change the one switch (calternal_mail_set_remote_content, MCPset_remote_content,calternal mail remote-content [true|false]).PATCH /preferencesaccepts either field alone, so changing the switch no longer overwrites read marking. An empty request returns 400.tools.test.tsclass,id,dir(ltr/rtl/auto) andlang(bounded) on adiv[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, sobody.xand#idrules, 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.tsContracts are regenerated:
openapi.json,actions.json,generated.ts,parity-matrix.md.docs/mcp.md,tests/adversarial/mail_api.mjsandxuser_matrix.pyare updated (the snapshot now coversmail_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 sharedcrates/calternal-plugin/src/raster_transport.rsexists only onjob/browserfix, which is not indev.job/browserfixmust merge first. Then rebase this branch and switch Mail's fetch toraster_transport::fetch_image,image_urlandtracking_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): cleancargo 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 outcargo test -p calternal-cli:test result: ok. 33 passed; 0 failed; 0 ignoredcargo test -p calternal-api:test result: ok. 15 passed; 0 failed/test result: ok. 9 passed; 0 failedcargo test -p calternal-server --bin calternal-server -- mcp contract openapi:test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 98 filtered outbun run check:COMPLETED 1998 FILES 0 ERRORS 0 WARNINGS 0 FILES_WITH_PROBLEMSTest Files 5 passed (5)/Tests 29 passed (29)Still to do in the merge round
cargo test -p calternal-serverwas not run; only the filtered set above ran.node apps/web/e2e/mail-html-726.mjsand add the two-tab revocation and body-class dark-CSS fixtures. Capture screenshots at 390, 820 and 1440 px in light and dark.mail_api.mjsandxuser_matrix.pyagainst a real server.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.
Review-fix follow-up on job/mailhtml-726. Starting head
b45b94fb07; merged origin/dev once at48c94c9776. Merge head188a06f95. 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.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: Finisheddevprofile [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.Resumed #726 on job/mailhtml-726 at
8e5777269f. Read CLAUDE.md, CONTEXT.md, DESIGN §45/§33 and all issue comments. Fetch origin/devbd11bacb51and browserfix58ffb09648. 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.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 commit2f8b0880b. Latest browserfix head91f4a4d60merged 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.
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:
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.READY FOR MERGE: no
Built
Branch
job/mailhtml-726; headdb9477547c29b74661687c8004145c19ab2c45b1. Integratedorigin/devatbd11bacb5189d39176e7cd48d5e977f1694321c1and browserfix through5774773d70e57894e4c29bfa58d286707c95eb27. 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, migrations0010–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}.tsand 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.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:calternal-plugin:calternal-api:calternal-plugin-calendar:calternal-cli:calternal-server:The focused 10 MiB HTML rejection regression, added after the Mail suite, returned:
Web
bun run check:Focused Vitest output:
Production web build passed. Focused live browser regression output:
Contract checks:
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
Known gaps / UX gaps left
Decisions
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:
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.mjsandnode 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. Followmacdav-lab/apple-interop-2026-10-02.mdunder 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 indocs/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/.