@rapidmx/web-client 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (443) hide show
  1. package/README.md +373 -88
  2. package/apps/admin/_layout.tsx +8 -2
  3. package/apps/admin/index.tsx +7 -1
  4. package/apps/admin/ingest-queue/index.tsx +2 -1
  5. package/apps/admin/mailboxes/[uid].tsx +23 -10
  6. package/apps/admin/quarantine/index.tsx +2 -1
  7. package/apps/admin/signing-certificates/index.tsx +343 -0
  8. package/apps/escrow/_layout.tsx +8 -2
  9. package/apps/shared/appearance/AppearanceForm.tsx +392 -0
  10. package/apps/shared/appearance/AppearanceHead.tsx +49 -0
  11. package/apps/shared/appearance/AppearanceProvider.tsx +470 -0
  12. package/apps/shared/appearance/ColorField.tsx +107 -0
  13. package/apps/shared/appearance/appearanceCache.ts +106 -0
  14. package/apps/shared/appearance/appearanceContext.ts +77 -0
  15. package/apps/shared/appearance/bootScript.ts +35 -0
  16. package/apps/shared/appearance/color.ts +169 -0
  17. package/apps/shared/appearance/photo.ts +88 -0
  18. package/apps/shared/appearance/resolvedTheme.ts +67 -0
  19. package/apps/shared/appearance/theme.ts +355 -0
  20. package/apps/shared/auth/accountUrl.ts +9 -0
  21. package/apps/shared/auth/adminAccess.ts +99 -0
  22. package/apps/shared/components/admin/layout/AdminShell.tsx +22 -10
  23. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +107 -60
  24. package/apps/shared/components/admin/settings/BrandingForm.tsx +24 -0
  25. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +2 -1
  26. package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
  27. package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
  28. package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
  29. package/apps/shared/components/calendar/layout/CalendarShell.tsx +8 -2
  30. package/apps/shared/components/contacts/ContactForm.tsx +11 -5
  31. package/apps/shared/components/contacts/ContactsSidebar.tsx +5 -3
  32. package/apps/shared/components/contacts/ContactsToolbar.tsx +98 -115
  33. package/apps/shared/components/contacts/layout/ContactsShell.tsx +25 -8
  34. package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
  35. package/apps/shared/components/layout/AppShell.tsx +477 -288
  36. package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
  37. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -389
  38. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -164
  39. package/apps/shared/components/layout/RailIcon.tsx +100 -0
  40. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
  41. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
  42. package/apps/shared/components/layout/UserMenu.tsx +372 -185
  43. package/apps/shared/components/mail/ConversationList.tsx +271 -262
  44. package/apps/shared/components/mail/ConversationThreadPane.tsx +147 -94
  45. package/apps/shared/components/mail/LazyReadingPane.tsx +113 -0
  46. package/apps/shared/components/mail/MailSelectionBar.tsx +227 -222
  47. package/apps/shared/components/mail/MessageDetailPane.tsx +1537 -1387
  48. package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
  49. package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
  50. package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -160
  51. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -418
  52. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1644 -1736
  53. package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +111 -0
  54. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -118
  55. package/apps/shared/components/mail/compose/composePerf.ts +46 -0
  56. package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
  57. package/apps/shared/components/mail/compose/quotedBody.ts +161 -100
  58. package/apps/shared/components/mail/layout/MailShell.tsx +490 -472
  59. package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
  60. package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
  61. package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
  62. package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
  63. package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
  64. package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
  65. package/apps/shared/components/mail/reading/color.ts +245 -0
  66. package/apps/shared/components/mail/reading/frameControl.ts +142 -0
  67. package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
  68. package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
  69. package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
  70. package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
  71. package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
  72. package/apps/shared/components/mail/unreadStyle.tsx +75 -0
  73. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
  74. package/apps/shared/components/settings/layout/SettingsShell.tsx +253 -240
  75. package/apps/shared/components/sharing/PrincipalPicker.tsx +145 -0
  76. package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
  77. package/apps/shared/components/tasks/layout/TasksShell.tsx +24 -8
  78. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -0
  79. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -0
  80. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -0
  81. package/apps/shared/keyboard/dispatch.ts +124 -0
  82. package/apps/shared/keyboard/format.ts +89 -0
  83. package/apps/shared/keyboard/keymap.ts +114 -0
  84. package/apps/shared/keyboard/match.ts +44 -0
  85. package/apps/shared/keyboard/parse.ts +136 -0
  86. package/apps/shared/keyboard/platform.ts +34 -0
  87. package/apps/shared/keyboard/registry.ts +65 -0
  88. package/apps/shared/keyboard/targets.ts +79 -0
  89. package/apps/shared/keyboard/useShortcut.ts +50 -0
  90. package/apps/shared/keyboard/useShortcutProps.ts +17 -0
  91. package/apps/shared/mail/folderCounts.ts +308 -0
  92. package/apps/shared/mail/folderTree.ts +143 -0
  93. package/apps/shared/mail/listSnapshots.ts +87 -0
  94. package/apps/shared/mail/messageReadState.ts +85 -0
  95. package/apps/shared/mail/newMailNotifications.ts +183 -0
  96. package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
  97. package/apps/shared/mail/outbox/composeBridge.ts +69 -0
  98. package/apps/shared/mail/outbox/outboxState.ts +88 -0
  99. package/apps/shared/mail/outbox/pendingSends.ts +146 -0
  100. package/apps/shared/mail/outbox/sendDecision.ts +157 -0
  101. package/apps/shared/mail/outbox/sendJob.ts +427 -0
  102. package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
  103. package/apps/shared/mail/outbox/sendState.ts +35 -0
  104. package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
  105. package/apps/shared/mail/useMailConnection.ts +203 -0
  106. package/apps/shared/mail/useMailLiveUpdates.ts +277 -224
  107. package/apps/shared/mail/useMarkMessageRead.ts +47 -0
  108. package/apps/shared/mail/useNewMailNotifications.ts +178 -0
  109. package/apps/shared/mail/useUnreadTitle.ts +42 -0
  110. package/apps/shared/navigation/AppRouter.tsx +300 -0
  111. package/apps/shared/navigation/appHrefs.ts +23 -0
  112. package/apps/shared/navigation/frameContext.tsx +35 -0
  113. package/apps/shared/navigation/idle.ts +45 -0
  114. package/apps/shared/navigation/routerContext.tsx +83 -0
  115. package/apps/shared/navigation/routes.ts +72 -0
  116. package/apps/shared/notifications/NotificationCenter.tsx +215 -0
  117. package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
  118. package/apps/shared/notifications/apiErrors.ts +98 -0
  119. package/apps/shared/notifications/headerOffset.ts +35 -0
  120. package/apps/shared/notifications/pushStatus.ts +56 -0
  121. package/apps/shared/notifications/store.ts +549 -0
  122. package/apps/shared/notifications/systemErrors.ts +63 -0
  123. package/apps/shared/notifications/useNotifications.ts +41 -0
  124. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -98
  125. package/apps/shared/signing/enrollmentStorage.ts +33 -0
  126. package/apps/shared/signing/enrollmentTracker.ts +385 -0
  127. package/apps/shared/signing/enrollmentView.ts +251 -0
  128. package/apps/shared/signing/signingInfo.ts +58 -0
  129. package/apps/shared/signing/useNow.ts +19 -0
  130. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
  131. package/apps/shared/styles/app.css +108 -10
  132. package/apps/www/_layout.tsx +8 -2
  133. package/apps/www/_routedPage.tsx +24 -0
  134. package/apps/www/_routes.ts +36 -0
  135. package/apps/www/calendar/index.tsx +64 -33
  136. package/apps/www/contacts/[uid].tsx +109 -107
  137. package/apps/www/contacts/index.tsx +62 -42
  138. package/apps/www/index.tsx +2291 -1917
  139. package/apps/www/messages/[uid].tsx +108 -101
  140. package/apps/www/settings/appearance/index.tsx +21 -0
  141. package/apps/www/settings/auto-reply/index.tsx +7 -8
  142. package/apps/www/settings/encryption/index.tsx +1287 -1249
  143. package/apps/www/settings/filters/[uid].tsx +7 -2
  144. package/apps/www/settings/filters/index.tsx +102 -99
  145. package/apps/www/settings/filters/new/index.tsx +140 -133
  146. package/apps/www/settings/labels/index.tsx +204 -201
  147. package/apps/www/settings/privacy/index.tsx +7 -9
  148. package/apps/www/settings/read-receipts/index.tsx +7 -8
  149. package/apps/www/settings/sharing/index.tsx +256 -274
  150. package/apps/www/settings/signatures/[uid].tsx +172 -167
  151. package/apps/www/settings/signatures/index.tsx +88 -85
  152. package/apps/www/settings/signatures/new/index.tsx +135 -129
  153. package/apps/www/tasks/index.tsx +29 -18
  154. package/dist/apps/admin/_layout.d.ts +5 -1
  155. package/dist/apps/admin/_layout.js +3 -2
  156. package/dist/apps/admin/index.js +3 -2
  157. package/dist/apps/admin/ingest-queue/index.js +2 -1
  158. package/dist/apps/admin/mailboxes/[uid].js +9 -6
  159. package/dist/apps/admin/quarantine/index.js +2 -1
  160. package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
  161. package/dist/apps/admin/signing-certificates/index.js +161 -0
  162. package/dist/apps/escrow/_layout.d.ts +5 -1
  163. package/dist/apps/escrow/_layout.js +3 -2
  164. package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
  165. package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
  166. package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
  167. package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
  168. package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
  169. package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
  170. package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
  171. package/dist/apps/shared/appearance/ColorField.js +40 -0
  172. package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
  173. package/dist/apps/shared/appearance/appearanceCache.js +80 -0
  174. package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
  175. package/dist/apps/shared/appearance/appearanceContext.js +29 -0
  176. package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
  177. package/dist/apps/shared/appearance/bootScript.js +33 -0
  178. package/dist/apps/shared/appearance/color.d.ts +65 -0
  179. package/dist/apps/shared/appearance/color.js +132 -0
  180. package/dist/apps/shared/appearance/photo.d.ts +17 -0
  181. package/dist/apps/shared/appearance/photo.js +79 -0
  182. package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
  183. package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
  184. package/dist/apps/shared/appearance/theme.d.ts +111 -0
  185. package/dist/apps/shared/appearance/theme.js +251 -0
  186. package/dist/apps/shared/auth/accountUrl.d.ts +2 -0
  187. package/dist/apps/shared/auth/accountUrl.js +8 -0
  188. package/dist/apps/shared/auth/adminAccess.d.ts +30 -0
  189. package/dist/apps/shared/auth/adminAccess.js +89 -0
  190. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  191. package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
  192. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
  193. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
  194. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
  195. package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
  196. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +1 -1
  197. package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
  198. package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
  199. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
  200. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +1 -1
  201. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +8 -4
  202. package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
  203. package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
  204. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +6 -2
  205. package/dist/apps/shared/components/contacts/ContactsToolbar.js +28 -10
  206. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +1 -1
  207. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +19 -6
  208. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
  209. package/dist/apps/shared/components/layout/AppShell.d.ts +34 -4
  210. package/dist/apps/shared/components/layout/AppShell.js +122 -17
  211. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
  212. package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
  213. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -9
  214. package/dist/apps/shared/components/layout/MailboxProvisioning.js +3 -1
  215. package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
  216. package/dist/apps/shared/components/layout/RailIcon.js +85 -0
  217. package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
  218. package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
  219. package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
  220. package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
  221. package/dist/apps/shared/components/layout/UserMenu.d.ts +25 -3
  222. package/dist/apps/shared/components/layout/UserMenu.js +88 -7
  223. package/dist/apps/shared/components/mail/ConversationList.js +5 -11
  224. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +6 -2
  225. package/dist/apps/shared/components/mail/ConversationThreadPane.js +87 -40
  226. package/dist/apps/shared/components/mail/LazyReadingPane.d.ts +9 -0
  227. package/dist/apps/shared/components/mail/LazyReadingPane.js +83 -0
  228. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +6 -4
  229. package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -4
  230. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +25 -8
  231. package/dist/apps/shared/components/mail/MessageDetailPane.js +167 -73
  232. package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
  233. package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
  234. package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
  235. package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
  236. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +38 -0
  237. package/dist/apps/shared/components/mail/compose/ComposeContext.js +97 -6
  238. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +4 -3
  239. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +3 -2
  240. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +316 -339
  241. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.d.ts +19 -0
  242. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +33 -0
  243. package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +9 -1
  244. package/dist/apps/shared/components/mail/compose/RichTextEditor.js +20 -2
  245. package/dist/apps/shared/components/mail/compose/composePerf.d.ts +16 -0
  246. package/dist/apps/shared/components/mail/compose/composePerf.js +41 -0
  247. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
  248. package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
  249. package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +13 -0
  250. package/dist/apps/shared/components/mail/compose/quotedBody.js +67 -11
  251. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +14 -4
  252. package/dist/apps/shared/components/mail/layout/MailShell.js +90 -104
  253. package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
  254. package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
  255. package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
  256. package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
  257. package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
  258. package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
  259. package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
  260. package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
  261. package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
  262. package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
  263. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
  264. package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
  265. package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
  266. package/dist/apps/shared/components/mail/reading/color.js +201 -0
  267. package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
  268. package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
  269. package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
  270. package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
  271. package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
  272. package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
  273. package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
  274. package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
  275. package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
  276. package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
  277. package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
  278. package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
  279. package/dist/apps/shared/components/mail/unreadStyle.d.ts +45 -0
  280. package/dist/apps/shared/components/mail/unreadStyle.js +61 -0
  281. package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
  282. package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
  283. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +1 -1
  284. package/dist/apps/shared/components/settings/layout/SettingsShell.js +16 -5
  285. package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +26 -0
  286. package/dist/apps/shared/components/sharing/PrincipalPicker.js +77 -0
  287. package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
  288. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +1 -1
  289. package/dist/apps/shared/components/tasks/layout/TasksShell.js +18 -6
  290. package/dist/apps/shared/keyboard/GlobalShortcuts.d.ts +14 -0
  291. package/dist/apps/shared/keyboard/GlobalShortcuts.js +37 -0
  292. package/dist/apps/shared/keyboard/ShortcutProvider.d.ts +20 -0
  293. package/dist/apps/shared/keyboard/ShortcutProvider.js +52 -0
  294. package/dist/apps/shared/keyboard/ShortcutsDialog.d.ts +14 -0
  295. package/dist/apps/shared/keyboard/ShortcutsDialog.js +42 -0
  296. package/dist/apps/shared/keyboard/dispatch.d.ts +16 -0
  297. package/dist/apps/shared/keyboard/dispatch.js +109 -0
  298. package/dist/apps/shared/keyboard/format.d.ts +12 -0
  299. package/dist/apps/shared/keyboard/format.js +74 -0
  300. package/dist/apps/shared/keyboard/keymap.d.ts +294 -0
  301. package/dist/apps/shared/keyboard/keymap.js +84 -0
  302. package/dist/apps/shared/keyboard/match.d.ts +20 -0
  303. package/dist/apps/shared/keyboard/match.js +31 -0
  304. package/dist/apps/shared/keyboard/parse.d.ts +31 -0
  305. package/dist/apps/shared/keyboard/parse.js +107 -0
  306. package/dist/apps/shared/keyboard/platform.d.ts +15 -0
  307. package/dist/apps/shared/keyboard/platform.js +22 -0
  308. package/dist/apps/shared/keyboard/registry.d.ts +39 -0
  309. package/dist/apps/shared/keyboard/registry.js +33 -0
  310. package/dist/apps/shared/keyboard/targets.d.ts +14 -0
  311. package/dist/apps/shared/keyboard/targets.js +67 -0
  312. package/dist/apps/shared/keyboard/useShortcut.d.ts +19 -0
  313. package/dist/apps/shared/keyboard/useShortcut.js +35 -0
  314. package/dist/apps/shared/keyboard/useShortcutProps.d.ts +10 -0
  315. package/dist/apps/shared/keyboard/useShortcutProps.js +15 -0
  316. package/dist/apps/shared/mail/folderCounts.d.ts +78 -0
  317. package/dist/apps/shared/mail/folderCounts.js +218 -0
  318. package/dist/apps/shared/mail/folderTree.d.ts +55 -0
  319. package/dist/apps/shared/mail/folderTree.js +116 -0
  320. package/dist/apps/shared/mail/listSnapshots.d.ts +46 -0
  321. package/dist/apps/shared/mail/listSnapshots.js +43 -0
  322. package/dist/apps/shared/mail/messageReadState.d.ts +32 -0
  323. package/dist/apps/shared/mail/messageReadState.js +63 -0
  324. package/dist/apps/shared/mail/newMailNotifications.d.ts +62 -0
  325. package/dist/apps/shared/mail/newMailNotifications.js +138 -0
  326. package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
  327. package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
  328. package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
  329. package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
  330. package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
  331. package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
  332. package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
  333. package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
  334. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
  335. package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
  336. package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
  337. package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
  338. package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
  339. package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
  340. package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
  341. package/dist/apps/shared/mail/outbox/sendState.js +8 -0
  342. package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
  343. package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
  344. package/dist/apps/shared/mail/useMailConnection.d.ts +56 -0
  345. package/dist/apps/shared/mail/useMailConnection.js +138 -0
  346. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +29 -9
  347. package/dist/apps/shared/mail/useMailLiveUpdates.js +60 -32
  348. package/dist/apps/shared/mail/useMarkMessageRead.d.ts +12 -0
  349. package/dist/apps/shared/mail/useMarkMessageRead.js +44 -0
  350. package/dist/apps/shared/mail/useNewMailNotifications.d.ts +35 -0
  351. package/dist/apps/shared/mail/useNewMailNotifications.js +130 -0
  352. package/dist/apps/shared/mail/useUnreadTitle.d.ts +18 -0
  353. package/dist/apps/shared/mail/useUnreadTitle.js +31 -0
  354. package/dist/apps/shared/navigation/AppRouter.d.ts +52 -0
  355. package/dist/apps/shared/navigation/AppRouter.js +242 -0
  356. package/dist/apps/shared/navigation/appHrefs.d.ts +14 -0
  357. package/dist/apps/shared/navigation/appHrefs.js +20 -0
  358. package/dist/apps/shared/navigation/frameContext.d.ts +19 -0
  359. package/dist/apps/shared/navigation/frameContext.js +24 -0
  360. package/dist/apps/shared/navigation/idle.d.ts +14 -0
  361. package/dist/apps/shared/navigation/idle.js +43 -0
  362. package/dist/apps/shared/navigation/routerContext.d.ts +37 -0
  363. package/dist/apps/shared/navigation/routerContext.js +56 -0
  364. package/dist/apps/shared/navigation/routes.d.ts +32 -0
  365. package/dist/apps/shared/navigation/routes.js +37 -0
  366. package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
  367. package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
  368. package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
  369. package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
  370. package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
  371. package/dist/apps/shared/notifications/apiErrors.js +86 -0
  372. package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
  373. package/dist/apps/shared/notifications/headerOffset.js +33 -0
  374. package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
  375. package/dist/apps/shared/notifications/pushStatus.js +54 -0
  376. package/dist/apps/shared/notifications/store.d.ts +136 -0
  377. package/dist/apps/shared/notifications/store.js +416 -0
  378. package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
  379. package/dist/apps/shared/notifications/systemErrors.js +58 -0
  380. package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
  381. package/dist/apps/shared/notifications/useNotifications.js +18 -0
  382. package/dist/apps/shared/search/LocalIndexLifecycle.js +17 -3
  383. package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
  384. package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
  385. package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
  386. package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
  387. package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
  388. package/dist/apps/shared/signing/enrollmentView.js +214 -0
  389. package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
  390. package/dist/apps/shared/signing/signingInfo.js +48 -0
  391. package/dist/apps/shared/signing/useNow.d.ts +2 -0
  392. package/dist/apps/shared/signing/useNow.js +18 -0
  393. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
  394. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
  395. package/dist/apps/shared/styles/app.css +108 -10
  396. package/dist/apps/www/_layout.d.ts +5 -1
  397. package/dist/apps/www/_layout.js +3 -2
  398. package/dist/apps/www/_routedPage.d.ts +12 -0
  399. package/dist/apps/www/_routedPage.js +19 -0
  400. package/dist/apps/www/_routes.d.ts +11 -0
  401. package/dist/apps/www/_routes.js +30 -0
  402. package/dist/apps/www/calendar/index.d.ts +2 -2
  403. package/dist/apps/www/calendar/index.js +41 -11
  404. package/dist/apps/www/contacts/[uid].d.ts +3 -9
  405. package/dist/apps/www/contacts/[uid].js +11 -9
  406. package/dist/apps/www/contacts/index.d.ts +2 -2
  407. package/dist/apps/www/contacts/index.js +30 -24
  408. package/dist/apps/www/index.d.ts +2 -2
  409. package/dist/apps/www/index.js +332 -48
  410. package/dist/apps/www/messages/[uid].d.ts +3 -7
  411. package/dist/apps/www/messages/[uid].js +10 -5
  412. package/dist/apps/www/settings/appearance/index.d.ts +4 -0
  413. package/dist/apps/www/settings/appearance/index.js +13 -0
  414. package/dist/apps/www/settings/auto-reply/index.d.ts +2 -2
  415. package/dist/apps/www/settings/auto-reply/index.js +6 -7
  416. package/dist/apps/www/settings/encryption/index.d.ts +2 -2
  417. package/dist/apps/www/settings/encryption/index.js +106 -96
  418. package/dist/apps/www/settings/filters/[uid].d.ts +2 -2
  419. package/dist/apps/www/settings/filters/[uid].js +6 -2
  420. package/dist/apps/www/settings/filters/index.d.ts +2 -2
  421. package/dist/apps/www/settings/filters/index.js +3 -1
  422. package/dist/apps/www/settings/filters/new/index.d.ts +2 -2
  423. package/dist/apps/www/settings/filters/new/index.js +9 -3
  424. package/dist/apps/www/settings/labels/index.d.ts +2 -2
  425. package/dist/apps/www/settings/labels/index.js +3 -1
  426. package/dist/apps/www/settings/privacy/index.d.ts +2 -2
  427. package/dist/apps/www/settings/privacy/index.js +8 -9
  428. package/dist/apps/www/settings/read-receipts/index.d.ts +2 -2
  429. package/dist/apps/www/settings/read-receipts/index.js +6 -7
  430. package/dist/apps/www/settings/sharing/index.d.ts +2 -2
  431. package/dist/apps/www/settings/sharing/index.js +22 -34
  432. package/dist/apps/www/settings/signatures/[uid].d.ts +2 -2
  433. package/dist/apps/www/settings/signatures/[uid].js +6 -2
  434. package/dist/apps/www/settings/signatures/index.d.ts +2 -2
  435. package/dist/apps/www/settings/signatures/index.js +3 -1
  436. package/dist/apps/www/settings/signatures/new/index.d.ts +2 -2
  437. package/dist/apps/www/settings/signatures/new/index.js +9 -4
  438. package/dist/apps/www/tasks/index.d.ts +2 -2
  439. package/dist/apps/www/tasks/index.js +22 -16
  440. package/package.json +2 -2
  441. package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
  442. package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
  443. package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
@@ -0,0 +1,298 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { sanitizeMessageBodyHtml } from "@rapidmx/react-shared/mail/messageBodySanitizer.js";
6
+ import { parseColour } from "./color.js";
7
+
8
+ /**
9
+ * Turning a message's HTML - which is hostile until proven otherwise - into the fragment that goes into its display frame.
10
+ *
11
+ * Four independent layers stand between a mail and script execution, and this file is two of them:
12
+ *
13
+ * 1. the server's own allow-list sanitizer (restapi's `ScanPipeline`), before the body is stored;
14
+ * 2. **this file**: DOMPurify (react-shared's `sanitizeMessageBodyHtml()` - scripts, `on*` handlers, `javascript:` URLs, and every
15
+ * remote resource reference removed) followed by a second, structural pass of our own that removes what DOMPurify's defaults
16
+ * allow but a mail body never needs (forms, frames, media, SVG's active elements, every non-`http(s)`/`mailto`/`tel` link);
17
+ * 3. the frame's Content-Security-Policy (`frameDocument.ts`): no script, no object, no frame, no form, nothing fetched;
18
+ * 4. the frame's `sandbox` attribute, which never has `allow-scripts`, so nothing executes even if all of the above failed.
19
+ *
20
+ * Nothing here parses into the app's own document: every parse is a `DOMParser` document, which has no browsing context, runs no
21
+ * script and fetches nothing.
22
+ */
23
+
24
+ /** A body longer than this many characters is not sanitized or shown inline - DOMPurify on megabytes of markup would freeze the tab. */
25
+ export const MAX_BODY_HTML_LENGTH = 1_500_000;
26
+ /** A body with more elements than this is not shown inline either: it cannot be laid out and adapted in a reasonable time. */
27
+ export const MAX_BODY_ELEMENTS = 20_000;
28
+
29
+ export interface PreparedBody {
30
+ /** `too_large` means `html` is empty and the caller should offer the message on its own page instead. */
31
+ status: "ok" | "too_large";
32
+ /** The sanitized fragment: one wrapper `div.rr-msg` holding the message's own `<style>` elements and body. */
33
+ html: string;
34
+ /** The mail carries its own dark styles: a `color-scheme` meta or property, or a `prefers-color-scheme: dark` block. */
35
+ declaresDarkSupport: boolean;
36
+ /** How many `<img>`s point at an inline part (`cid:`) - the frame's CSP only needs an image source for the attachments then. */
37
+ inlineImages: number;
38
+ }
39
+
40
+ export interface PrepareOptions {
41
+ /** The URL an inline image's `cid:` reference stands for, or `undefined` when there is no such part. Only `data:` URIs of an
42
+ * image type and `http(s)` URLs are accepted; anything else is treated as unresolved. */
43
+ resolveCid?: (contentId: string) => string | undefined;
44
+ }
45
+
46
+ /** What a stylesheet or a `style` attribute may not contain, whatever the sanitizers before it did. */
47
+ const STYLE_HAZARDS: [RegExp, string][] = [
48
+ // Fixed and sticky positioning could cover the frame (or, with a viewport-sized box, all of it) with a fake dialog.
49
+ [/position\s*:\s*(?:fixed|sticky)/gi, "position:relative"],
50
+ // A length in viewport-height units is the frame's own height, which is the message's height: `min-height: 100vh` would make the frame grow
51
+ // by what its content adds on every measurement. As plain pixels, nothing in the message depends on the size of the frame it is in.
52
+ [/(\d(?:\.\d+)?)\s*(?:[sld]?v(?:h|min|max|b))\b/gi, "$1px"],
53
+ [/expression\s*\(/gi, "none("],
54
+ [/-moz-binding\s*:/gi, "-x-binding:"],
55
+ [/behavior\s*:/gi, "x-behavior:"],
56
+ [/@import[^;]*;?/gi, ""],
57
+ ];
58
+
59
+ /** CSS with the constructs above neutralised. */
60
+ export function sanitizeCss(css: string): string {
61
+ return STYLE_HAZARDS.reduce((text, [pattern, replacement]) => text.replace(pattern, replacement), css);
62
+ }
63
+
64
+ /** Elements removed together with everything inside them. */
65
+ const REMOVED_ELEMENTS = new Set([
66
+ "script",
67
+ "noscript",
68
+ "object",
69
+ "embed",
70
+ "applet",
71
+ "iframe",
72
+ "frame",
73
+ "frameset",
74
+ "noembed",
75
+ "noframes",
76
+ "audio",
77
+ "video",
78
+ "source",
79
+ "track",
80
+ "canvas",
81
+ "dialog",
82
+ "template",
83
+ "slot",
84
+ "portal",
85
+ "bgsound",
86
+ "xml",
87
+ "base",
88
+ "meta",
89
+ "link",
90
+ "title",
91
+ "head",
92
+ "input",
93
+ "select",
94
+ "textarea",
95
+ "datalist",
96
+ "option",
97
+ "optgroup",
98
+ "math",
99
+ // SVG's active or reference-following elements: a script, a foreign document, a use of another element, and animation
100
+ // (which can set an `href` or an event handler's target).
101
+ "foreignobject",
102
+ "use",
103
+ "set",
104
+ "animate",
105
+ "animatemotion",
106
+ "animatetransform",
107
+ ]);
108
+
109
+ /** Elements dropped but not their content: a message wrapped in an old ASP.NET `<form>` is still a message. */
110
+ const UNWRAPPED_ELEMENTS = new Set(["form", "fieldset", "legend", "label", "button", "output", "marquee", "blink"]);
111
+
112
+ /** Attributes no element of a mail may keep, on top of every `on*` handler. */
113
+ const REMOVED_ATTRIBUTES = new Set([
114
+ "name",
115
+ "srcdoc",
116
+ "formaction",
117
+ "form",
118
+ "ping",
119
+ "nonce",
120
+ "is",
121
+ "slot",
122
+ "contenteditable",
123
+ "accesskey",
124
+ "tabindex",
125
+ "autofocus",
126
+ "draggable",
127
+ "srcset",
128
+ "crossorigin",
129
+ "integrity",
130
+ "referrerpolicy",
131
+ "http-equiv",
132
+ "xlink:href",
133
+ ]);
134
+
135
+ /** The `href`s of a link a reader can follow: nothing that runs, nothing relative to this app, nothing that reads a local file. */
136
+ const LINK_URL = /^(?:https?:\/\/|mailto:|tel:)/i;
137
+ /** Inline images: raster types and SVG (an image context cannot run script or load anything), embedded as data. */
138
+ const IMAGE_DATA_URI = /^data:image\/(?:png|jpe?g|gif|webp|avif|bmp|svg\+xml)[;,]/i;
139
+ const CSS_ESCAPE = /\\([0-9a-f]{1,6})\s?|\\(.)/gi;
140
+
141
+ /** Decodes CSS escapes, so a check for `color-scheme` cannot be dodged with `col\6fr-scheme`. */
142
+ function unescapeCss(css: string): string {
143
+ return css.replace(CSS_ESCAPE, (_match, hex: string | undefined, other: string | undefined) =>
144
+ hex ? String.fromCodePoint(Math.min(parseInt(hex, 16), 0x10ffff)) : (other as string),
145
+ );
146
+ }
147
+
148
+ /**
149
+ * Whether a mail's own markup declares that it supports a dark scheme: `<meta name="color-scheme">` naming dark,
150
+ * a `color-scheme` property naming dark in a stylesheet or a `style` attribute, or an `@media (prefers-color-scheme: dark)` block. Such a mail
151
+ * brings its own dark styles, which the display honours instead of adapting the mail's colours.
152
+ */
153
+ export function declaresDarkSupport(doc: Document): boolean {
154
+ const meta = doc.querySelector('meta[name="color-scheme" i]');
155
+ if (/\bdark\b/i.test(meta?.getAttribute("content") ?? "")) {
156
+ return true;
157
+ }
158
+ // An element's text is never null, and a `[style]` element always has the attribute.
159
+ const sheets = Array.from(doc.querySelectorAll("style"), (style) => style.textContent);
160
+ const inline = Array.from(doc.querySelectorAll("[style]"), (element) => element.getAttribute("style")!);
161
+ return [...sheets, ...inline].some((css) => {
162
+ const text = unescapeCss(css);
163
+ return /color-scheme\s*:[^;}]*\bdark\b/i.test(text) || /prefers-color-scheme\s*:\s*dark/i.test(text);
164
+ });
165
+ }
166
+
167
+ /** A `bgcolor`/`text`/`link` attribute's value as an `rgb()` colour if we can read it - nothing else is ever copied into a style.
168
+ * `bgcolor="ffffff"` (no hash) is what old templates write and what a browser accepts. */
169
+ function attributeColour(value: string | null): string | undefined {
170
+ const text = (value ?? "").trim();
171
+ const colour = parseColour(/^[0-9a-f]{6}$/i.test(text) ? `#${text}` : text);
172
+ return colour && colour.a > 0 ? `rgb(${Math.round(colour.r)}, ${Math.round(colour.g)}, ${Math.round(colour.b)})` : undefined;
173
+ }
174
+
175
+ /** The wrapper `div.rr-msg`: the message's head `<style>`s first (DOMPurify drops a leading one as head content), then its body, with
176
+ * what a `<body bgcolor text link>` said carried over as the styles those attributes stand for. */
177
+ function wrapMessage(doc: Document): string {
178
+ const wrapper = doc.createElement("div");
179
+ wrapper.className = "rr-msg";
180
+ for (const style of Array.from(doc.head.querySelectorAll("style"))) {
181
+ wrapper.appendChild(style.cloneNode(true));
182
+ }
183
+ const [background, text, link] = ["bgcolor", "text", "link"].map((name) => attributeColour(doc.body.getAttribute(name)));
184
+ // What the `<body>` said about itself moves to the wrapper, which stands in for it: its presentational colours first (a `style` beats
185
+ // them), then its own `style`, its classes (a template's stylesheet may select on them) and its direction.
186
+ const declarations = [background && `background-color:${background}`, text && `color:${text}`, doc.body.getAttribute("style")].filter(Boolean);
187
+ if (declarations.length > 0) {
188
+ wrapper.setAttribute("style", declarations.join(";"));
189
+ }
190
+ const bodyClass = doc.body.getAttribute("class");
191
+ if (bodyClass) {
192
+ wrapper.className += ` ${bodyClass}`;
193
+ }
194
+ const direction = doc.body.getAttribute("dir");
195
+ if (direction) {
196
+ wrapper.setAttribute("dir", direction);
197
+ }
198
+ if (link) {
199
+ const rule = doc.createElement("style");
200
+ rule.textContent = `:where(.rr-msg a:link){color:${link}}`;
201
+ wrapper.appendChild(rule);
202
+ }
203
+ wrapper.append(...Array.from(doc.body.childNodes));
204
+ return wrapper.outerHTML;
205
+ }
206
+
207
+ /** Replaces `element` with its children. */
208
+ function unwrap(element: Element): void {
209
+ element.replaceWith(...Array.from(element.childNodes));
210
+ }
211
+
212
+ function hardenLink(element: Element): void {
213
+ const href = element.getAttribute("href")?.trim() ?? "";
214
+ if (LINK_URL.test(href)) {
215
+ element.setAttribute("href", href);
216
+ element.setAttribute("target", "_blank");
217
+ element.setAttribute("rel", "noopener noreferrer nofollow");
218
+ } else {
219
+ // A link that goes nowhere a reader could safely follow stays as text, without the address it hid.
220
+ element.removeAttribute("href");
221
+ element.removeAttribute("target");
222
+ }
223
+ }
224
+
225
+ function hardenImage(element: Element, options: PrepareOptions): void {
226
+ const src = (element.getAttribute("src") ?? "").trim();
227
+ const inline = /^cid:/i.test(src);
228
+ const target = inline ? options.resolveCid?.(src.slice(4).replace(/^<|>$/g, "")) : src;
229
+ // Remote images are never fetched (they would tell the sender the message was read); an inline image resolves to an embedded
230
+ // image or to this server's own attachment URL, and one that doesn't resolve has nothing to show.
231
+ if (target !== undefined && (IMAGE_DATA_URI.test(target) || (inline && /^https?:\/\//i.test(target)))) {
232
+ element.setAttribute("src", target);
233
+ } else {
234
+ element.removeAttribute("src");
235
+ }
236
+ }
237
+
238
+ /**
239
+ * The second, structural pass over already-sanitized markup: everything a mail body has no use for is removed or unwrapped, every
240
+ * link is forced to open in a new tab without an opener, and hazards in stylesheets are neutralised. Runs on a detached document.
241
+ */
242
+ function harden(body: HTMLElement, options: PrepareOptions): { elements: number; inlineImages: number } {
243
+ let inlineImages = 0;
244
+ const all = Array.from(body.querySelectorAll("*"));
245
+ for (const element of all) {
246
+ const tag = element.localName.toLowerCase();
247
+ if (REMOVED_ELEMENTS.has(tag)) {
248
+ element.remove();
249
+ continue;
250
+ }
251
+ if (UNWRAPPED_ELEMENTS.has(tag)) {
252
+ unwrap(element);
253
+ continue;
254
+ }
255
+ for (const attribute of Array.from(element.attributes)) {
256
+ const name = attribute.name.toLowerCase();
257
+ if (name.startsWith("on") || REMOVED_ATTRIBUTES.has(name)) {
258
+ element.removeAttribute(attribute.name);
259
+ } else if (name === "style") {
260
+ element.setAttribute("style", sanitizeCss(attribute.value));
261
+ }
262
+ }
263
+ if (tag === "style") {
264
+ element.textContent = sanitizeCss(element.textContent);
265
+ } else if (tag === "a" || tag === "area") {
266
+ hardenLink(element);
267
+ } else if (tag === "img") {
268
+ if (/^cid:/i.test(element.getAttribute("src") ?? "")) {
269
+ inlineImages++;
270
+ }
271
+ hardenImage(element, options);
272
+ }
273
+ }
274
+ return { elements: all.length, inlineImages };
275
+ }
276
+
277
+ /**
278
+ * The display fragment for a message's HTML - see the file's header. `too_large` (and an empty `html`) for a body over
279
+ * `MAX_BODY_HTML_LENGTH` characters or `MAX_BODY_ELEMENTS` elements; an empty fragment where there is no DOM to parse with.
280
+ */
281
+ export function prepareBodyHtml(raw: string, options: PrepareOptions = {}): PreparedBody {
282
+ const empty = { html: "", declaresDarkSupport: false, inlineImages: 0 };
283
+ if (raw.length > MAX_BODY_HTML_LENGTH) {
284
+ return { status: "too_large", ...empty };
285
+ }
286
+ if (typeof DOMParser === "undefined") {
287
+ return { status: "ok", ...empty };
288
+ }
289
+ const parsed = new DOMParser().parseFromString(raw, "text/html");
290
+ const darkSupport = declaresDarkSupport(parsed);
291
+ const sanitized = sanitizeMessageBodyHtml(wrapMessage(parsed));
292
+ const cleaned = new DOMParser().parseFromString(`<!doctype html><body>${sanitized}`, "text/html").body;
293
+ const { elements, inlineImages } = harden(cleaned, options);
294
+ if (elements > MAX_BODY_ELEMENTS) {
295
+ return { status: "too_large", ...empty };
296
+ }
297
+ return { status: "ok", html: cleaned.innerHTML, declaresDarkSupport: darkSupport, inlineImages };
298
+ }
@@ -0,0 +1,245 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+
6
+ /** A colour as the browser reports it: channels 0-255 and an alpha 0-1. */
7
+ export interface Rgba {
8
+ r: number;
9
+ g: number;
10
+ b: number;
11
+ a: number;
12
+ }
13
+
14
+ /** A colour with no transparency worth speaking of - what a contrast ratio is computed between. */
15
+ export type Rgb = Pick<Rgba, "r" | "g" | "b">;
16
+
17
+ /** The handful of named colours a `bgcolor`/`text` attribute is realistically written with. Anything else is not guessed at. */
18
+ const NAMED_COLOURS: Record<string, [number, number, number]> = {
19
+ white: [255, 255, 255],
20
+ black: [0, 0, 0],
21
+ red: [255, 0, 0],
22
+ green: [0, 128, 0],
23
+ blue: [0, 0, 255],
24
+ yellow: [255, 255, 0],
25
+ gray: [128, 128, 128],
26
+ grey: [128, 128, 128],
27
+ silver: [192, 192, 192],
28
+ navy: [0, 0, 128],
29
+ teal: [0, 128, 128],
30
+ maroon: [128, 0, 0],
31
+ purple: [128, 0, 128],
32
+ orange: [255, 165, 0],
33
+ lime: [0, 255, 0],
34
+ aqua: [0, 255, 255],
35
+ fuchsia: [255, 0, 255],
36
+ olive: [128, 128, 0],
37
+ };
38
+
39
+ function clamp(value: number, min: number, max: number): number {
40
+ return Math.min(max, Math.max(min, value));
41
+ }
42
+
43
+ /** One `rgb()` channel: a number 0-255, or a percentage of it. */
44
+ function channel(token: string): number {
45
+ return token.endsWith("%") ? (parseFloat(token) / 100) * 255 : parseFloat(token);
46
+ }
47
+
48
+ /** An alpha: a number 0-1, or a percentage. */
49
+ function alpha(token: string | undefined): number {
50
+ if (token === undefined) {
51
+ return 1;
52
+ }
53
+ return token.endsWith("%") ? parseFloat(token) / 100 : parseFloat(token);
54
+ }
55
+
56
+ /**
57
+ * Reads the colour notations a browser's computed style and an old-fashioned `bgcolor` attribute produce: `#rgb`, `#rgba`,
58
+ * `#rrggbb`, `#rrggbbaa`, `rgb()`/`rgba()` in the comma and the space syntax, `color(srgb ...)`, `transparent` and a few names.
59
+ * `undefined` for anything else (a gradient, `currentcolor`, `oklch()`), which callers treat as "unknown, leave it alone".
60
+ */
61
+ export function parseColour(input: string | undefined | null): Rgba | undefined {
62
+ const text = (input ?? "").trim().toLowerCase();
63
+ if (text === "transparent") {
64
+ return { r: 0, g: 0, b: 0, a: 0 };
65
+ }
66
+ if (text in NAMED_COLOURS) {
67
+ const [r, g, b] = NAMED_COLOURS[text];
68
+ return { r, g, b, a: 1 };
69
+ }
70
+ const hex = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.exec(text)?.[1];
71
+ if (hex) {
72
+ const full = hex.length <= 4 ? [...hex].map((c) => c + c).join("") : hex;
73
+ const [r, g, b, a] = [0, 2, 4, 6].map((i) => (full.length > i ? parseInt(full.slice(i, i + 2), 16) : 255));
74
+ return { r, g, b, a: a / 255 };
75
+ }
76
+ const functional = /^(rgba?|color)\(\s*(?:srgb\s+)?([^)]*)\)$/.exec(text);
77
+ if (functional) {
78
+ const parts = functional[2].split(/[\s,/]+/).filter(Boolean);
79
+ if (parts.length < 3 || parts.length > 4 || parts.some((part) => !/^[-+]?(\d+\.?\d*|\.\d+)%?$/.test(part))) {
80
+ return undefined;
81
+ }
82
+ const scale = functional[1] === "color" ? 255 : 1;
83
+ const [r, g, b] = parts.slice(0, 3).map((part) => clamp(channel(part) * scale, 0, 255));
84
+ return { r, g, b, a: clamp(alpha(parts[3]), 0, 1) };
85
+ }
86
+ return undefined;
87
+ }
88
+
89
+ /** `rgb(r, g, b)` for an opaque colour, `rgba(...)` otherwise - what goes into a generated stylesheet. */
90
+ export function formatColour(colour: Rgba | Rgb): string {
91
+ const { r, g, b } = colour;
92
+ const a = "a" in colour ? colour.a : 1;
93
+ const rgb = [r, g, b].map((value) => Math.round(clamp(value, 0, 255))).join(", ");
94
+ return a >= 1 ? `rgb(${rgb})` : `rgba(${rgb}, ${Math.round(a * 1000) / 1000})`;
95
+ }
96
+
97
+ /** `top` painted over `bottom`, as the opaque colour that results. */
98
+ export function composite(top: Rgba, bottom: Rgb): Rgb {
99
+ return {
100
+ r: top.r * top.a + bottom.r * (1 - top.a),
101
+ g: top.g * top.a + bottom.g * (1 - top.a),
102
+ b: top.b * top.a + bottom.b * (1 - top.a),
103
+ };
104
+ }
105
+
106
+ function linear(value: number): number {
107
+ const c = value / 255;
108
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
109
+ }
110
+
111
+ /** WCAG relative luminance, 0 (black) to 1 (white). */
112
+ export function luminance(colour: Rgb): number {
113
+ return 0.2126 * linear(colour.r) + 0.7152 * linear(colour.g) + 0.0722 * linear(colour.b);
114
+ }
115
+
116
+ /** WCAG contrast ratio between two opaque colours, 1 to 21. */
117
+ export function contrastRatio(a: Rgb, b: Rgb): number {
118
+ const [lighter, darker] = [luminance(a), luminance(b)].sort((x, y) => y - x);
119
+ return (lighter + 0.05) / (darker + 0.05);
120
+ }
121
+
122
+ /** The lowest contrast ratio WCAG accepts for body text. */
123
+ export const MIN_TEXT_CONTRAST = 4.5;
124
+
125
+ export interface Hsl {
126
+ h: number;
127
+ s: number;
128
+ l: number;
129
+ }
130
+
131
+ export function rgbToHsl({ r, g, b }: Rgb): Hsl {
132
+ const [rn, gn, bn] = [r / 255, g / 255, b / 255];
133
+ const max = Math.max(rn, gn, bn);
134
+ const min = Math.min(rn, gn, bn);
135
+ const l = (max + min) / 2;
136
+ if (max === min) {
137
+ return { h: 0, s: 0, l };
138
+ }
139
+ const d = max - min;
140
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
141
+ const h = max === rn ? (gn - bn) / d + (gn < bn ? 6 : 0) : max === gn ? (bn - rn) / d + 2 : (rn - gn) / d + 4;
142
+ return { h: h * 60, s, l };
143
+ }
144
+
145
+ export function hslToRgb({ h, s, l }: Hsl): Rgb {
146
+ const a = s * Math.min(l, 1 - l);
147
+ const f = (n: number) => {
148
+ const k = (n + h / 30) % 12;
149
+ return 255 * (l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)));
150
+ };
151
+ return { r: f(0), g: f(8), b: f(4) };
152
+ }
153
+
154
+ /** Whether two colours are the same to the eye's tolerance - what "unchanged" means when deciding to emit a rule. */
155
+ export function sameColour(a: Rgb, b: Rgb): boolean {
156
+ return Math.abs(a.r - b.r) < 1 && Math.abs(a.g - b.g) < 1 && Math.abs(a.b - b.b) < 1;
157
+ }
158
+
159
+ /**
160
+ * `colour` moved along its own hue and saturation - lighter over a dark background, darker over a light one - until it reaches
161
+ * `min` contrast against `background`. A colour that already has it is returned untouched; one that cannot get there (a mid-grey
162
+ * background) becomes whichever of white and black reads better.
163
+ */
164
+ export function ensureContrast(colour: Rgb, background: Rgb, min = MIN_TEXT_CONTRAST): Rgb {
165
+ // Judged as the whole-number colour it will be written as: a fractional colour that reads can round to one that does not.
166
+ const whole = roundColour(colour);
167
+ if (contrastRatio(whole, background) >= min) {
168
+ return whole;
169
+ }
170
+ const target = luminance(background) < 0.5 ? 1 : 0;
171
+ const hsl = rgbToHsl(colour);
172
+ if (contrastRatio(hslToRgb({ ...hsl, l: target }), background) < min) {
173
+ return blackOrWhite(background);
174
+ }
175
+ let [near, far] = [hsl.l, target];
176
+ for (let i = 0; i < 16; i++) {
177
+ const middle = (near + far) / 2;
178
+ if (contrastRatio(roundColour(hslToRgb({ ...hsl, l: middle })), background) >= min) {
179
+ far = middle;
180
+ } else {
181
+ near = middle;
182
+ }
183
+ }
184
+ return roundColour(hslToRgb({ ...hsl, l: far }));
185
+ }
186
+
187
+ function roundColour({ r, g, b }: Rgb): Rgb {
188
+ return { r: Math.round(r), g: Math.round(g), b: Math.round(b) };
189
+ }
190
+
191
+ /** How far from grey a colour may be and still count as a neutral (black, white, greys) rather than a colour with a hue to keep. */
192
+ const NEUTRAL_SATURATION = 0.12;
193
+
194
+ /** The theme a message is shown in: the opaque surface behind the body, its text and link colours, and whether it is a dark one. */
195
+ export interface ThemeSurface {
196
+ background: Rgb;
197
+ text: Rgb;
198
+ link: Rgb;
199
+ dark: boolean;
200
+ }
201
+
202
+ /** Whichever of black and white reads better on `background`. */
203
+ export function blackOrWhite(background: Rgb): Rgb {
204
+ const [white, black] = [
205
+ { r: 255, g: 255, b: 255 },
206
+ { r: 0, g: 0, b: 0 },
207
+ ];
208
+ return contrastRatio(white, background) >= contrastRatio(black, background) ? white : black;
209
+ }
210
+
211
+ /**
212
+ * An authored text colour that cannot be read against the theme's background, adapted so it can. A neutral (black, white, a
213
+ * grey) is flipped to the other end of the scale - black becomes the theme's text colour, a dark grey a light one, and in a
214
+ * light theme white becomes dark text - by placing it between the theme's text and background colours according to how far it
215
+ * is from the one it must not be. Any other colour keeps its hue and saturation and only has its lightness moved, as little as
216
+ * it takes to reach 4.5:1 (`ensureContrast()`), so navy stays a navy and a brand red stays red. One that already reads is
217
+ * returned untouched.
218
+ */
219
+ export function adaptForeground(colour: Rgb, surface: ThemeSurface): Rgb {
220
+ if (contrastRatio(colour, surface.background) >= MIN_TEXT_CONTRAST) {
221
+ return colour;
222
+ }
223
+ const hsl = rgbToHsl(colour);
224
+ if (hsl.s >= NEUTRAL_SATURATION) {
225
+ return ensureContrast(colour, surface.background);
226
+ }
227
+ // The distance from the end of the scale that would have been readable: a dark theme wants light text, so it is how dark
228
+ // the colour is; a light theme, how light.
229
+ const t = surface.dark ? hsl.l : 1 - hsl.l;
230
+ const mixed = {
231
+ r: surface.text.r + (surface.background.r - surface.text.r) * t,
232
+ g: surface.text.g + (surface.background.g - surface.text.g) * t,
233
+ b: surface.text.b + (surface.background.b - surface.text.b) * t,
234
+ };
235
+ return ensureContrast(mixed, surface.background);
236
+ }
237
+
238
+ /**
239
+ * The first colour in a computed `background-image` (a gradient's first stop): the best guess at what text over a gradient sits
240
+ * on. `undefined` for an image (`url(...)`) or anything with no colour in it.
241
+ */
242
+ export function firstColourStop(backgroundImage: string): Rgba | undefined {
243
+ const match = /rgba?\([^)]*\)|#[0-9a-f]{3,8}\b/i.exec(backgroundImage);
244
+ return match ? parseColour(match[0]) : undefined;
245
+ }