@rapidmx/web-client 0.10.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 (310) hide show
  1. package/README.md +183 -2
  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/components/admin/layout/AdminShell.tsx +22 -10
  21. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +107 -60
  22. package/apps/shared/components/admin/settings/BrandingForm.tsx +24 -0
  23. package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
  24. package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
  25. package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
  26. package/apps/shared/components/contacts/ContactForm.tsx +11 -5
  27. package/apps/shared/components/contacts/ContactsSidebar.tsx +5 -3
  28. package/apps/shared/components/contacts/ContactsToolbar.tsx +25 -52
  29. package/apps/shared/components/contacts/layout/ContactsShell.tsx +10 -5
  30. package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
  31. package/apps/shared/components/layout/AppShell.tsx +98 -32
  32. package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
  33. package/apps/shared/components/layout/RailIcon.tsx +100 -0
  34. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
  35. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
  36. package/apps/shared/components/layout/UserMenu.tsx +75 -6
  37. package/apps/shared/components/mail/ConversationList.tsx +4 -2
  38. package/apps/shared/components/mail/ConversationThreadPane.tsx +487 -475
  39. package/apps/shared/components/mail/LazyReadingPane.tsx +14 -16
  40. package/apps/shared/components/mail/MailSelectionBar.tsx +2 -10
  41. package/apps/shared/components/mail/MessageDetailPane.tsx +366 -287
  42. package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
  43. package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
  44. package/apps/shared/components/mail/compose/ComposeContext.tsx +35 -2
  45. package/apps/shared/components/mail/compose/ComposeWindow.tsx +239 -437
  46. package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
  47. package/apps/shared/components/mail/layout/MailShell.tsx +39 -25
  48. package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
  49. package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
  50. package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
  51. package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
  52. package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
  53. package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
  54. package/apps/shared/components/mail/reading/color.ts +245 -0
  55. package/apps/shared/components/mail/reading/frameControl.ts +142 -0
  56. package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
  57. package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
  58. package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
  59. package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
  60. package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
  61. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
  62. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
  63. package/apps/shared/components/sharing/PrincipalPicker.tsx +145 -0
  64. package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
  65. package/apps/shared/components/tasks/layout/TasksShell.tsx +9 -5
  66. package/apps/shared/mail/folderCounts.ts +9 -3
  67. package/apps/shared/mail/folderTree.ts +143 -0
  68. package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
  69. package/apps/shared/mail/outbox/composeBridge.ts +69 -0
  70. package/apps/shared/mail/outbox/outboxState.ts +88 -0
  71. package/apps/shared/mail/outbox/pendingSends.ts +146 -0
  72. package/apps/shared/mail/outbox/sendDecision.ts +157 -0
  73. package/apps/shared/mail/outbox/sendJob.ts +427 -0
  74. package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
  75. package/apps/shared/mail/outbox/sendState.ts +35 -0
  76. package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
  77. package/apps/shared/mail/useMailConnection.ts +81 -28
  78. package/apps/shared/mail/useMailLiveUpdates.ts +55 -10
  79. package/apps/shared/mail/useNewMailNotifications.ts +47 -32
  80. package/apps/shared/navigation/AppRouter.tsx +2 -2
  81. package/apps/shared/notifications/NotificationCenter.tsx +215 -0
  82. package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
  83. package/apps/shared/notifications/apiErrors.ts +98 -0
  84. package/apps/shared/notifications/headerOffset.ts +35 -0
  85. package/apps/shared/notifications/pushStatus.ts +56 -0
  86. package/apps/shared/notifications/store.ts +549 -0
  87. package/apps/shared/notifications/systemErrors.ts +63 -0
  88. package/apps/shared/notifications/useNotifications.ts +41 -0
  89. package/apps/shared/signing/enrollmentStorage.ts +33 -0
  90. package/apps/shared/signing/enrollmentTracker.ts +385 -0
  91. package/apps/shared/signing/enrollmentView.ts +251 -0
  92. package/apps/shared/signing/signingInfo.ts +58 -0
  93. package/apps/shared/signing/useNow.ts +19 -0
  94. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
  95. package/apps/shared/styles/app.css +80 -0
  96. package/apps/www/_layout.tsx +8 -2
  97. package/apps/www/_routes.ts +1 -0
  98. package/apps/www/calendar/index.tsx +9 -14
  99. package/apps/www/contacts/[uid].tsx +5 -8
  100. package/apps/www/contacts/index.tsx +587 -584
  101. package/apps/www/index.tsx +38 -27
  102. package/apps/www/messages/[uid].tsx +3 -1
  103. package/apps/www/settings/appearance/index.tsx +21 -0
  104. package/apps/www/settings/auto-reply/index.tsx +3 -7
  105. package/apps/www/settings/encryption/index.tsx +151 -116
  106. package/apps/www/settings/filters/[uid].tsx +3 -1
  107. package/apps/www/settings/filters/new/index.tsx +3 -1
  108. package/apps/www/settings/privacy/index.tsx +3 -8
  109. package/apps/www/settings/read-receipts/index.tsx +3 -7
  110. package/apps/www/settings/sharing/index.tsx +44 -65
  111. package/apps/www/settings/signatures/[uid].tsx +3 -1
  112. package/apps/www/settings/signatures/new/index.tsx +3 -2
  113. package/apps/www/tasks/index.tsx +11 -16
  114. package/dist/apps/admin/_layout.d.ts +5 -1
  115. package/dist/apps/admin/_layout.js +3 -2
  116. package/dist/apps/admin/index.js +3 -2
  117. package/dist/apps/admin/ingest-queue/index.js +2 -1
  118. package/dist/apps/admin/mailboxes/[uid].js +9 -6
  119. package/dist/apps/admin/quarantine/index.js +2 -1
  120. package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
  121. package/dist/apps/admin/signing-certificates/index.js +161 -0
  122. package/dist/apps/escrow/_layout.d.ts +5 -1
  123. package/dist/apps/escrow/_layout.js +3 -2
  124. package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
  125. package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
  126. package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
  127. package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
  128. package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
  129. package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
  130. package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
  131. package/dist/apps/shared/appearance/ColorField.js +40 -0
  132. package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
  133. package/dist/apps/shared/appearance/appearanceCache.js +80 -0
  134. package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
  135. package/dist/apps/shared/appearance/appearanceContext.js +29 -0
  136. package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
  137. package/dist/apps/shared/appearance/bootScript.js +33 -0
  138. package/dist/apps/shared/appearance/color.d.ts +65 -0
  139. package/dist/apps/shared/appearance/color.js +132 -0
  140. package/dist/apps/shared/appearance/photo.d.ts +17 -0
  141. package/dist/apps/shared/appearance/photo.js +79 -0
  142. package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
  143. package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
  144. package/dist/apps/shared/appearance/theme.d.ts +111 -0
  145. package/dist/apps/shared/appearance/theme.js +251 -0
  146. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  147. package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
  148. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
  149. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
  150. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
  151. package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
  152. package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
  153. package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
  154. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
  155. package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
  156. package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
  157. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  158. package/dist/apps/shared/components/contacts/ContactsToolbar.js +24 -9
  159. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +4 -1
  160. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
  161. package/dist/apps/shared/components/layout/AppShell.d.ts +6 -1
  162. package/dist/apps/shared/components/layout/AppShell.js +59 -13
  163. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
  164. package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
  165. package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
  166. package/dist/apps/shared/components/layout/RailIcon.js +85 -0
  167. package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
  168. package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
  169. package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
  170. package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
  171. package/dist/apps/shared/components/layout/UserMenu.d.ts +7 -1
  172. package/dist/apps/shared/components/layout/UserMenu.js +41 -7
  173. package/dist/apps/shared/components/mail/ConversationList.js +3 -2
  174. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +2 -1
  175. package/dist/apps/shared/components/mail/ConversationThreadPane.js +42 -20
  176. package/dist/apps/shared/components/mail/LazyReadingPane.js +9 -8
  177. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +3 -4
  178. package/dist/apps/shared/components/mail/MailSelectionBar.js +2 -3
  179. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +18 -7
  180. package/dist/apps/shared/components/mail/MessageDetailPane.js +86 -41
  181. package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
  182. package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
  183. package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
  184. package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
  185. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +8 -0
  186. package/dist/apps/shared/components/mail/compose/ComposeContext.js +28 -3
  187. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +2 -1
  188. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +229 -328
  189. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
  190. package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
  191. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +2 -0
  192. package/dist/apps/shared/components/mail/layout/MailShell.js +20 -14
  193. package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
  194. package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
  195. package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
  196. package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
  197. package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
  198. package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
  199. package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
  200. package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
  201. package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
  202. package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
  203. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
  204. package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
  205. package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
  206. package/dist/apps/shared/components/mail/reading/color.js +201 -0
  207. package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
  208. package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
  209. package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
  210. package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
  211. package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
  212. package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
  213. package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
  214. package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
  215. package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
  216. package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
  217. package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
  218. package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
  219. package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
  220. package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
  221. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
  222. package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +26 -0
  223. package/dist/apps/shared/components/sharing/PrincipalPicker.js +77 -0
  224. package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
  225. package/dist/apps/shared/components/tasks/layout/TasksShell.js +3 -1
  226. package/dist/apps/shared/mail/folderCounts.d.ts +1 -1
  227. package/dist/apps/shared/mail/folderCounts.js +9 -3
  228. package/dist/apps/shared/mail/folderTree.d.ts +55 -0
  229. package/dist/apps/shared/mail/folderTree.js +116 -0
  230. package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
  231. package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
  232. package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
  233. package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
  234. package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
  235. package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
  236. package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
  237. package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
  238. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
  239. package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
  240. package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
  241. package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
  242. package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
  243. package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
  244. package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
  245. package/dist/apps/shared/mail/outbox/sendState.js +8 -0
  246. package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
  247. package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
  248. package/dist/apps/shared/mail/useMailConnection.d.ts +9 -8
  249. package/dist/apps/shared/mail/useMailConnection.js +64 -22
  250. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +17 -4
  251. package/dist/apps/shared/mail/useMailLiveUpdates.js +36 -10
  252. package/dist/apps/shared/mail/useNewMailNotifications.d.ts +4 -10
  253. package/dist/apps/shared/mail/useNewMailNotifications.js +43 -23
  254. package/dist/apps/shared/navigation/AppRouter.js +2 -2
  255. package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
  256. package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
  257. package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
  258. package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
  259. package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
  260. package/dist/apps/shared/notifications/apiErrors.js +86 -0
  261. package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
  262. package/dist/apps/shared/notifications/headerOffset.js +33 -0
  263. package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
  264. package/dist/apps/shared/notifications/pushStatus.js +54 -0
  265. package/dist/apps/shared/notifications/store.d.ts +136 -0
  266. package/dist/apps/shared/notifications/store.js +416 -0
  267. package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
  268. package/dist/apps/shared/notifications/systemErrors.js +58 -0
  269. package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
  270. package/dist/apps/shared/notifications/useNotifications.js +18 -0
  271. package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
  272. package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
  273. package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
  274. package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
  275. package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
  276. package/dist/apps/shared/signing/enrollmentView.js +214 -0
  277. package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
  278. package/dist/apps/shared/signing/signingInfo.js +48 -0
  279. package/dist/apps/shared/signing/useNow.d.ts +2 -0
  280. package/dist/apps/shared/signing/useNow.js +18 -0
  281. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
  282. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
  283. package/dist/apps/shared/styles/app.css +80 -0
  284. package/dist/apps/www/_layout.d.ts +5 -1
  285. package/dist/apps/www/_layout.js +3 -2
  286. package/dist/apps/www/_routes.js +1 -0
  287. package/dist/apps/www/calendar/index.js +3 -4
  288. package/dist/apps/www/contacts/[uid].js +5 -7
  289. package/dist/apps/www/contacts/index.js +14 -21
  290. package/dist/apps/www/index.js +37 -19
  291. package/dist/apps/www/messages/[uid].js +3 -1
  292. package/dist/apps/www/settings/appearance/index.d.ts +4 -0
  293. package/dist/apps/www/settings/appearance/index.js +13 -0
  294. package/dist/apps/www/settings/auto-reply/index.js +3 -6
  295. package/dist/apps/www/settings/encryption/index.js +103 -95
  296. package/dist/apps/www/settings/filters/[uid].js +3 -1
  297. package/dist/apps/www/settings/filters/new/index.js +3 -1
  298. package/dist/apps/www/settings/privacy/index.js +5 -8
  299. package/dist/apps/www/settings/read-receipts/index.js +3 -6
  300. package/dist/apps/www/settings/sharing/index.js +19 -33
  301. package/dist/apps/www/settings/signatures/[uid].js +3 -1
  302. package/dist/apps/www/settings/signatures/new/index.js +3 -2
  303. package/dist/apps/www/tasks/index.js +8 -14
  304. package/package.json +2 -2
  305. package/apps/shared/components/mail/NewMailToasts.tsx +0 -141
  306. package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
  307. package/dist/apps/shared/components/mail/NewMailToasts.d.ts +0 -19
  308. package/dist/apps/shared/components/mail/NewMailToasts.js +0 -55
  309. package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
  310. package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
package/README.md CHANGED
@@ -119,10 +119,185 @@ How it behaves:
119
119
  names are unchanged (`useShortcutProps()` gives a control both).
120
120
 
121
121
  Known limits: a browser keeps a few keys for itself (`Ctrl+N`, `Ctrl+T`, `Ctrl+W` are never delivered to a page, and some browsers claim
122
- `Ctrl+Shift+A/B/C/M/S`); and a message body is shown in a sandboxed iframe, which forwards no key events, so after clicking into a
123
- message body press Tab or click the list before using a shortcut. In the compose body the editor's own bindings win over `Ctrl+Shift+S`, `B`
122
+ `Ctrl+Shift+A/B/C/M/S`); a message body is shown in a frame of its own, which hands the keys pressed in it on to the layer, so they work there too. In the compose body the editor's own bindings win over `Ctrl+Shift+S`, `B`
124
123
  and `L` (strike-through, quote, align left).
125
124
 
125
+ ## The reading pane
126
+
127
+ `shared/components/mail/` draws a message as a **card** and a conversation as a stack of them, under a **subject card** that stays pinned at the top
128
+ while the cards scroll (`MessageDetailPane`, `ConversationThreadPane`, and `reading/` for the pieces):
129
+
130
+ - **Subject card** - the conversation's (or the message's) subject as the page's `h1`, "N messages", a note when a very long thread was cut. In a
131
+ thread it is on screen at once, from what the list row already knows, with a skeleton card per message (up to three) until the messages arrive.
132
+ - **Message card** - rounded, bordered and lifted, in the app's own tokens (`bg-surface`, `text-text`, `border-border`), so it follows the theme, a
133
+ branding palette and the Appearance colours. A header row with the sender's initials, `Name <address>`, To/Cc/Bcc, the time and the actions (Reply,
134
+ Reply All, Forward, Archive, Move to, Labels, and "View original" when it applies); the security badge and every notice (signature, key change,
135
+ receipts, Outbox state); attachments; the body; a slim "You replied to this message." bar; and, on a single message and on the newest open one in a
136
+ thread, Reply and Forward at its foot. An older message of a thread is a collapsed card (sender, date, first line) that expands on click or Enter
137
+ (`aria-expanded`, `aria-controls`; the focus stays on the message's header button as it swaps between the two states).
138
+ - **Each card is exactly as tall as its message.** The body is shown at its natural height - no inner scrollbar, no dead space, no fixed height - and
139
+ follows it as images load and the width changes. A table or image wider than the card scrolls **inside** the card and never widens the pane. The
140
+ pane scrolls as a whole. Everything is on screen before the body: the header card, the message card's header and a skeleton where the body will be.
141
+
142
+ - **Encrypted messages** are themed card content in the same place as the body, never a frame: while the security state is being worked out, a skeleton;
143
+ locked (no unlocked keys on this device), a lock, "This message is encrypted", "Unlock your keys to read it" and an **Unlock** button that asks for the unlock
144
+ prompt and, once unlocked - by it or from anywhere - decrypts the message in place and moves the focus to it; unreadable with the keys in hand (not a recipient,
145
+ damaged), "This message can’t be decrypted" with the reason and no button; decrypted, the message like any other. The placeholder subject `[...]` reads
146
+ "Encrypted message", and list rows and collapsed cards with no preview say "Encrypted message" with a small lock.
147
+
148
+ **How a body is shown.** The server sanitizes a message's HTML when it ingests it (`GET /mail/messages/:id/content`); the client then treats what it
149
+ receives - and everything it recovers by decrypting or verifying, which the server never saw - as hostile. It is sanitized again with DOMPurify (scripts,
150
+ event handlers, `javascript:` URLs, every remote resource), stripped of forms, frames, media, plug-ins, SVG's active elements and every link that is not
151
+ `http(s)`/`mailto`/`tel`, and shown in a **sandboxed iframe without `allow-scripts`**, behind a Content-Security-Policy that allows no script, object, frame,
152
+ form or fetch (images are `data:` URIs and this server's own attachment URLs for inline `cid:` images; **remote images are never loaded**). `allow-same-origin`
153
+ is what lets the app measure the frame and adapt its colours, and it is safe only because nothing can run there: any one of the four layers (server
154
+ sanitizer, client sanitizer, CSP, sandbox) stops a script on its own - see `.claude/NOTES.md`, 2026-09-21, for the threat model and the hostile-mail corpus it
155
+ was tried against. Every link opens in a new tab without an opener. A message over 1.5 MB of markup or 20,000 elements is offered on its own page instead.
156
+
157
+ **Mail takes the theme.** A message follows the app's scheme wherever its author left the colours to the reader, and is shown as authored where the
158
+ author chose them. Plain-text mail and HTML with no colours at all (a typed reply) are drawn in the theme's surface and text colours, links in its link
159
+ colour. Where the HTML sets a colour: text on the theme's surface is kept if it reads (4.5:1) and adapted if it doesn't (black becomes white in a dark
160
+ theme, white becomes dark in a light one, a low-contrast navy is only lightened as far as it takes); anything on an authored, opaque background - a white table,
161
+ a banner, a gradient - is shown exactly as authored, except text nobody coloured, which gets black or white by contrast with that background, never the theme's
162
+ colour. A message that declares its own dark styles (a `color-scheme` meta or property, or a `prefers-color-scheme: dark` block) gets them when the theme is
163
+ dark. "View original" (the sun, in the card's header; the moon then goes back) shows just that message exactly as authored for the rest of the session; it is
164
+ offered only where adapting changes something. Known limits: a dark logo or PNG with a transparent background on a dark theme, text baked into images,
165
+ `mix-blend-mode` and SVG paint are left as they are.
166
+
167
+ Printing puts a message back to the colours it was written with for the print and adapts it again afterwards; the actions and the footer are not printed.
168
+
169
+ ## Notifications, sending and the Outbox
170
+
171
+ **One pop-up system** (`shared/notifications/`). `AppChrome` mounts `NotificationCenter` once, right under the header row (a zero-height `sticky`
172
+ line that sticks just below the header - the title bar's or a branding header's height, published as `--rr-header-h` - so the stack starts below the
173
+ account menu and never covers it, the header's buttons or a compose window's title bar and Send button; its height is capped by `--rr-compose-top`), and
174
+ everything the app has to say goes through the framework-free store:
175
+
176
+ ```ts
177
+ import { notify, update, dismiss } from "@rapidmx/web-client/shared/notifications/store.js";
178
+ import { notifyApiError } from "@rapidmx/web-client/shared/notifications/apiErrors.js";
179
+
180
+ const id = notify({ kind: "error", title: "Couldn't archive the message", message: "The server said no.",
181
+ details: ["Status: 502 (api-1)"], actions: [{ label: "Retry", onClick: retry }], dedupeKey: "archive" });
182
+ update(id, { kind: "success", title: "Archived", actions: [] }); // resolve it
183
+ try { await archive(); } catch (err) { notifyApiError(err, "Couldn't archive the message"); }
184
+ ```
185
+
186
+ `kind` is `mail`, `info`, `success`, `warning` or `error`. At most three show at once (the rest queue); errors and anything with `actions` are sticky
187
+ until dismissed or resolved by `update()`/`dismiss()`, the others go after 5 to 10 seconds - clocks pause on hover, focus and a hidden tab; a repeated
188
+ `dedupeKey` is one pop-up with a count; errors are announced assertively (`role="alert"` inside an `aria-live="assertive"` region), the rest politely;
189
+ `details` is an expandable, monospace, copyable block. The last 30 (not new-mail ones) are kept in memory and `sessionStorage` and listed by "Recent
190
+ notifications" in the account menu. `notifyApiError()` turns an `ApiRequestError` into an error pop-up with the server's message (a `401` becomes
191
+ "Your session expired" with a Sign in button, anywhere in the app: the frame registers `setApiUnauthorizedObserver()`). The frame also catches unhandled
192
+ errors and rejections as one "Something went wrong", and says so, subtly, when the live connection has been down for more than ten seconds.
193
+
194
+ **Sending does not wait** (`shared/mail/outbox/`). Send validates what it can from what the window knows (a recipient, keys that must be unlocked, an
195
+ attachment that can't be signed - shown inline, the window stays open; an attachment still uploading is waited for), then closes the window at once and
196
+ hands a snapshot to `startSend()`: save, sign or encrypt in the browser if that applies, then `POST /mail/messages/:id/send` with `{ "background": true }`,
197
+ which answers `202 { status: "queued", message }` and relays in the background. Failures at any stage are a sticky "This message wasn't sent" with the
198
+ reason, technical details, **Retry** and **Open draft**; the server's later outcome arrives as `send-succeeded` / `send-retrying` / `send-failed` push
199
+ events. The Outbox row in the folder list is a pill with the count (optimistic at the click, then the server's), animated while anything is on its way and
200
+ red while one failed; rows in the Outbox list say what each message is doing.
201
+
202
+ **Encryption fails open.** A message is treated as unencrypted - its draft saves, Close and Send work, nothing about encryption is shown - unless you turned
203
+ encryption on (or replied to an encrypted message) or the encryption policy *was loaded* and encrypts for every recipient; a request that failed, is slow or
204
+ is pending never blocks anything (`compose/encryptionRequirement.ts`).
205
+
206
+ ## The header, the footer and their variables
207
+
208
+ The admin console's Branding page sets a **header** and a **footer** as HTML. The server sanitizes them when they are saved and this client
209
+ sanitizes them again when it shows them: no scripts, styles, forms, inputs or SVG, no `on*` handlers, only `http`, `https` and `mailto` links.
210
+
211
+ **A custom header is the top of the app.** When one is set it replaces the app's own title bar (the app's name and the account menu) and the icon
212
+ at the top of the icon rail: the rail then starts with the app icons, and the header stays at the top of the window while a page scrolls. The
213
+ account menu moves into it. Two variables can be written in the *text* of the header or the footer:
214
+
215
+ | Variable | Replaced with |
216
+ | --- | --- |
217
+ | `{USER_MENU}` | The account menu: the avatar button and its drop-down (Account, Settings, notifications, keyboard shortcuts, Admin Console, Sign Out). |
218
+ | `{APP_TITLE}` | The name of the app on screen ("Mail", "Calendar", "Contacts", "Tasks", "Settings"), kept current as you move between apps without a page load. |
219
+
220
+ ```html
221
+ <div style="display:flex; align-items:center; justify-content:space-between; padding:0.5rem 1rem">
222
+ <span>{APP_TITLE}</span>
223
+ <span>{USER_MENU}</span>
224
+ </div>
225
+ ```
226
+
227
+ - A variable is matched exactly - upper case, in braces, no spaces - and only in text. One in an attribute (`title="{USER_MENU}"`), a comment,
228
+ a `<script>` or `<style>` is left as written or gone with them, and the author can't write the placeholders React fills in (`data-rr-slot` is
229
+ stripped).
230
+ - `{USER_MENU}` is replaced **once**, where it first appears in the header; if the header has none, at its first place in the footer (the menu then
231
+ opens upward); if neither has one, the menu sits in a small cell at the right end of the header, so the account menu, Sign Out and the shortcuts
232
+ help are never lost. A second or third copy is removed (two menus would mean duplicate ids and focus targets), and so is one inside a link or in
233
+ text that can't hold an element (`<title>`, `<textarea>`).
234
+ - `{APP_TITLE}` is replaced **everywhere** it appears, as text (never markup). It changes on client-side navigation, and works in the footer too.
235
+ - The menu is drawn into `<body>` with `position: fixed`, so a header with `overflow: hidden`, its own `z-index` or a stacking context can neither
236
+ clip it nor hide it behind the page. It follows its button when the window resizes or scrolls, and fits a 390 px window.
237
+ - Put `{USER_MENU}` where your CSS doesn't hide it: a header whose stylesheet hides its `<nav>` on phones should keep the variable outside the nav.
238
+ - Focus order is document order: the menu button is reached after the text before it and before what follows it.
239
+ - Pages that don't use a shell (the booking pages) render `BrandingHeader` and `BrandingFooter`, which have no menu: `{USER_MENU}` renders nothing
240
+ there and `{APP_TITLE}` is the `appTitle` prop, if any. The variable names are `USER_MENU_VARIABLE` and `APP_TITLE_VARIABLE` in
241
+ `shared/components/layout/BrandingChrome.js`.
242
+
243
+ The frame draws the header from the `branding` prop the server rendered the page with (`useBranding()`'s own fetch takes over when it answers), so
244
+ the frame is the right shape - no title bar, no rail icon - from the first paint, and the icon of a frame without a custom header sits flush with the
245
+ top of the window (`RailIcon`: it is measured on a canvas and raised by whatever empty margin the image file has above its artwork).
246
+
247
+ ## Appearance
248
+
249
+ **Settings > Appearance** (`/settings/appearance`) lets each user choose the colour scheme (System, Light or Dark), four theme colours (primary,
250
+ accent, surface and text - each a colour picker with a hex field and its own "Reset"), and a background (none, a colour, or an uploaded picture with
251
+ dim 0-80%, blur 0-20 px and fit cover, contain or tile). Every change is applied to the real app in the same frame - the page you are on is the preview -
252
+ and saved in the background (coalesced, only what changed); a save that fails puts the change back and says so. A picture is checked before it is
253
+ uploaded (PNG, JPEG, WebP or AVIF, up to 8 MB). Text on the surface, and text on an accent button, under 4.5:1 contrast gets a warning, never a block.
254
+ "Reset all" returns everything to the defaults and removes the picture.
255
+
256
+ **How it reaches the app.** `AppearanceProvider` (mounted by `AppChrome`, `AdminShell` and `EscrowShell`) owns the preferences and writes one
257
+ `<style id="rr-appearance">` into `<head>` (`appearance/theme.js`), sets `<html data-theme="light|dark">` (for System, the operating system's scheme,
258
+ live) and remembers the result in `localStorage` (`rapidmx-appearance`). The server's `_layout.tsx` renders the same stylesheet from the `appearance`
259
+ page prop and a tiny inline script (`APPEARANCE_BOOT_SCRIPT`) applies the browser's copy before the first paint, so there is no flash on a load and none
260
+ on a client-side navigation. The server caches a rendered page for up to a minute, so whichever copy - the page's or the browser's - was changed last wins,
261
+ and the server is asked once the page is up.
262
+
263
+ - **Precedence:** the app's defaults < the deployment's branding stylesheet < the user's choices. A user's colours are `--rr-color-*` custom properties
264
+ declared `!important` on `<html>`, so no branding rule can beat them; the branding stylesheet's own rules (say a header's background) still decide their
265
+ own elements, exactly as before. Only the colours a user chose are set: primary and accent get their full scales (darker and lighter steps mixed from the
266
+ one colour) and readable text colours, surface and text get their alt, border and muted derivatives.
267
+ - **The background** is two fixed pseudo-elements of `<html>` (`::before` the picture or colour, `::after` the dim, the scheme's surface colour at the
268
+ chosen opacity), behind everything, with no markup, no layout shift and nothing to repaint while a list scrolls. With a background set the icon rail, the
269
+ header and the content region become translucent panels (`--color-surface` and `--color-surface-alt` are the surface colour at an alpha, chosen so text
270
+ and muted text stay at least 4.5:1 against the worst place the picture can be - the picture's lightest and darkest twentieth, measured on a canvas -
271
+ more see-through for a plain photo or a high dim, more opaque for a busy one); menus, dialogs, drawers and pop-ups take the opaque colours back. Without a
272
+ background nothing changes.
273
+ - **Hooks:** `useResolvedTheme()` (`appearance/resolvedTheme.js`) is `"light"` or `"dark"`, live, needing no provider; `useAppearance()` gives
274
+ `{ prefs, resolved, setPrefs(patch), uploadBackground(file), removeBackground(), reset(), saving, error, ... }`.
275
+ - **API** (`@rapidmx/react-shared/appearance/preferencesApi.js`): `GET/PUT /mail/preferences/appearance` (a merge), `POST/DELETE
276
+ /mail/preferences/appearance/background`, `GET .../background/:version`; a change is also pushed as `AppearancePreferences...` `update` events on
277
+ the user's own uid channel.
278
+
279
+ ## Toolbars, folders and the signing certificate
280
+
281
+ **Toolbars that fit (`components/layout/ResponsiveToolbar.tsx`).** A bar measures its own width (`ResizeObserver`) and lays itself out from a table of actions (`ToolbarAction`: label,
282
+ icon, group, `rank` - the lowest goes into "More" first - and `essential`): captions under the icons while everything fits; else icons only (each keeps a tooltip and, for New contact, its
283
+ shortcut's `title` and `aria-keyshortcuts`); else as many icons as fit and the rest in a portalled `role="menu"` behind a **More** button. `layoutToolbar(actions, width)` is the pure
284
+ function behind it (`width === undefined` - server render, no ResizeObserver - shows everything), and the bar is `overflow-x-clip` so it can never paint over the pane beside it. Contacts
285
+ uses it; the panes around it follow one rule: a list column of `clamp(16rem, 32vw, 26rem)`, a detail pane that keeps the rest, each column scrolling by itself (`md:h-[calc(100dvh - var(--rr-header-h))]` on the
286
+ page root - the frame's own height is open-ended), the side menu a drawer below `lg`, and no empty shell column for a single mailbox.
287
+
288
+ **The folder tree (`mail/folderTree.ts`).** `FOLDER_ORDER` (Inbox, Drafts, Outbox, Sent Items, Deleted Items, Junk, Archive, then custom), `upsertFolder()`, `removeFolder()` and
289
+ `reconcileFolders()` return the same array when nothing changed (everything that reads the tree starts over when it is a new one), never touch the counts (the overlay in `folderCounts.ts`
290
+ owns them) and never remove on a listing (deletions arrive as events). `useMailConnection()` files what `Folder` create / update / delete events say and what every counts read-back lists
291
+ (`useFolderCounts(mailboxes, folders, onFoldersListed)`), so a folder is found by an event, a message in it, a send outcome, the poll, a focus or a reconnect, whichever comes first. `folderRows()`
292
+ is the one place a folder that has not arrived is drawn - a muted Outbox / Sent Items placeholder while a message is on its way - so it can never be drawn twice. Push channels are ordered inboxes,
293
+ mailbox uids, then the other folders in sidebar order (`pushChannelsFor()`).
294
+
295
+ **The signing certificate (`signing/`).** `enrollmentTracker.ts` follows one enrollment per mailbox for everybody who wants to show or react to it: a first read, then 15 s, 30 s and every 60 s
296
+ while pending - only while the page is visible (a hidden tab asks nothing; coming back or a focus asks at once and restarts the backoff), stopping when it ends; `checkEnrollmentNow()` is the
297
+ "Check status" (a 10 s cooldown, the server's 429 honoured, a plain read on a server without the check endpoint); a pending enrollment stays followed after the page that started it is left.
298
+ `useSigningEnrollmentWatcher()` (in `AppChrome`) seeds it for the mailboxes you own and raises the "issued" / "failed" pop-up. `enrollmentView.ts` holds the wording, the steps, the percentage
299
+ and the expiry maths; `SigningCertificateCard` draws them. The account menu's **Theme** row is `ThemeSwitch`, over `useAppearance().setPrefs({ mode })`.
300
+
126
301
  ## Plugin UI surface
127
302
 
128
303
  Server plugins can ship their own pages (see the plugin manifest's `ui` field in `@rapidmx/restapi`). The server builds
@@ -182,6 +357,12 @@ use:
182
357
  - `components/overlays/Modal.js` and `components/overlays/Drawer.js`;
183
358
  - `components/pickers/MiniDatePicker.js`.
184
359
 
360
+ ## Uninstalling a plugin with its data
361
+
362
+ The admin console's Plugins page (`PluginsManager`, also embedded in the setup wizard) uninstalls a plugin through a dialog with an unchecked **Also delete all data this plugin stored** box. Ticking it lists what will be deleted (collections and tables, saved settings, cached package and pages, whatever the plugin cleans up itself), says **This can't be undone**, turns the confirm button into the red **Uninstall and delete data** and keeps it disabled until the plugin's display name is typed (case and surrounding spaces ignored). The dialog is a `Modal` (focus moves in and stays in, Escape closes, the checkbox is described by the list) and a `<form>`, so Enter in the name field confirms once it matches; at 390 px it keeps the modal's 20 px gutters and the list wraps.
363
+
364
+ The request is `removePlugin(uid, { purgeData: true })` (`@rapidmx/react-shared/admin/pluginsApi.js`); the server accepts it only from an elevated administrator, and an `api-104` answer is shown as a request to reload or sign in again. Deletion happens on the servers after the last copy stops running the plugin, so `GET /api/system/plugins/status` carries each deletion as `purges` and the page lists the uninstalled plugin with its state - *Uninstalled - data will be deleted after servers restart* (and how many servers still run it), *Data deleted <date>*, or *Data deletion failed: <reason>* with the failed steps and a Retry button. It reads the status every 5 seconds while a deletion is waiting or running, and raises an `apps/shared/notifications` pop-up when one that was under way is deleted or fails, and a warning when adding a plugin cancels one. A plugin that is installed is never shown as uninstalled, whatever the server still lists about an earlier deletion.
365
+
185
366
  ## Development
186
367
 
187
368
  ```sh
@@ -4,15 +4,20 @@
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React, { PropsWithChildren } from "react";
6
6
  import { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
7
+ import { AppearanceHead, appearanceHtmlAttributes } from "../shared/appearance/AppearanceHead.js";
7
8
  import { CUSTOM_STYLESHEET_LINK_ID } from "@rapidmx/react-shared/branding/useBranding.js";
8
9
 
9
10
  export interface LayoutProps {
10
11
  /** See `apps/www/_layout.tsx`'s `LayoutProps` doc comment - identical mechanism, supplied by
11
12
  * `AdminConsoleRoute`'s `fetchProps()` override. */
12
13
  branding?: Branding;
14
+ /** The signed-in user's stored appearance preferences (a page prop from the same `fetchProps()`, present only on the www pages and only when the user has any): rendered as a stylesheet in the `<head>` so the very first paint is themed - see `AppearanceHead`. */
15
+ appearance?: unknown;
16
+ /** The signed-in user (supplied by the framework), so a cached appearance from another account on this browser is never applied. */
17
+ userUid?: string;
13
18
  }
14
19
 
15
- export default function Layout({ children, branding }: PropsWithChildren<LayoutProps>) {
20
+ export default function Layout({ children, branding, appearance, userUid }: PropsWithChildren<LayoutProps>) {
16
21
  const title = branding?.title || branding?.companyName
17
22
  ? `${branding?.title || branding?.companyName}: Mail Admin Console`
18
23
  : "RapidMX: Mail Admin Console";
@@ -20,7 +25,7 @@ export default function Layout({ children, branding }: PropsWithChildren<LayoutP
20
25
  const stylesheetHref = branding?.stylesheetUrl;
21
26
 
22
27
  return (
23
- <html lang="en">
28
+ <html lang="en" {...appearanceHtmlAttributes(appearance, userUid)}>
24
29
  <head>
25
30
  <meta charSet="utf-8" />
26
31
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -34,6 +39,7 @@ export default function Layout({ children, branding }: PropsWithChildren<LayoutP
34
39
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
35
40
  />
36
41
  {stylesheetHref && <link rel="stylesheet" href={stylesheetHref} id={CUSTOM_STYLESHEET_LINK_ID} />}
42
+ <AppearanceHead appearance={appearance} />
37
43
  </head>
38
44
  <body>{children}</body>
39
45
  </html>
@@ -31,7 +31,8 @@ function MailboxesListContent() {
31
31
  useEffect(() => {
32
32
  setLoading(true);
33
33
  setError(null);
34
- listMailboxes({ page, limit: PAGE_SIZE })
34
+ // The administration scope: every mailbox as administrative metadata - the console never shows anybody's mail.
35
+ listMailboxes({ page, limit: PAGE_SIZE, scope: "admin" })
35
36
  .then(setMailboxes)
36
37
  .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load mailboxes."))
37
38
  .finally(() => setLoading(false));
@@ -69,6 +70,11 @@ function MailboxesListContent() {
69
70
  </div>
70
71
  </div>
71
72
 
73
+ <p className="text-sm text-text-muted mb-4">
74
+ Administrative details only (addresses, owners, quota). The console never shows a mailbox&rsquo;s mail:
75
+ to see someone&rsquo;s account, impersonate them from their mailbox page.
76
+ </p>
77
+
72
78
  {error && <Alert>{error}</Alert>}
73
79
 
74
80
  <Modal open={confirmingSetup} onClose={() => setConfirmingSetup(false)} title="Run setup again?">
@@ -50,7 +50,8 @@ function IngestQueueContent() {
50
50
  let cancelled = false;
51
51
  setLoading(true);
52
52
  setError(null);
53
- listIngestQueue(mailboxUid, { page, limit: PAGE_SIZE })
53
+ // The administration scope: any mailbox's pending mail, recorded in the audit log.
54
+ listIngestQueue(mailboxUid, { page, limit: PAGE_SIZE, scope: "admin" })
54
55
  .then((data) => {
55
56
  if (!cancelled) setEntries(data);
56
57
  })
@@ -23,12 +23,16 @@ function formatBytes(bytes: number): string {
23
23
  export default function MailboxDetailPage(props: Omit<AdminShellProps, "active"> & { params: { uid: string } }) {
24
24
  return (
25
25
  <AdminShell {...props} active="mailboxes">
26
- <MailboxDetailContent uid={props.params.uid} impersonationBaseUrl={props.impersonationBaseUrl} />
26
+ <MailboxDetailContent uid={props.params.uid} impersonationBaseUrl={props.impersonationBaseUrl} currentUserUid={props.userUid} />
27
27
  </AdminShell>
28
28
  );
29
29
  }
30
30
 
31
- function MailboxDetailContent({ uid, impersonationBaseUrl }: { uid: string } & Pick<AdminShellProps, "impersonationBaseUrl">) {
31
+ function MailboxDetailContent({
32
+ uid,
33
+ impersonationBaseUrl,
34
+ currentUserUid,
35
+ }: { uid: string; currentUserUid?: string } & Pick<AdminShellProps, "impersonationBaseUrl">) {
32
36
  const [mailbox, setMailbox] = useState<Mailbox | null>(null);
33
37
  const [loading, setLoading] = useState(true);
34
38
  const [error, setError] = useState<string | null>(null);
@@ -47,7 +51,7 @@ function MailboxDetailContent({ uid, impersonationBaseUrl }: { uid: string } & P
47
51
  setAccessError(null);
48
52
  }
49
53
 
50
- // Only ever invoked from the access-confirmation modal below, which only renders for a loaded mailbox that has
54
+ // Only ever invoked from the impersonation-confirmation modal below, which only renders for a loaded mailbox that has
51
55
  // an owner. A failure stays in the modal rather than replacing the whole page.
52
56
  async function handleAccessMailbox() {
53
57
  setImpersonating(true);
@@ -56,7 +60,7 @@ function MailboxDetailContent({ uid, impersonationBaseUrl }: { uid: string } & P
56
60
  await impersonateUser(impersonationBaseUrl ?? "", mailbox!.ownerUserUid!);
57
61
  window.location.href = "/";
58
62
  } catch (err) {
59
- setAccessError(err instanceof ApiRequestError ? err.message : "Could not access this mailbox.");
63
+ setAccessError(err instanceof ApiRequestError ? err.message : "Could not impersonate this user.");
60
64
  setImpersonating(false);
61
65
  }
62
66
  }
@@ -86,7 +90,8 @@ function MailboxDetailContent({ uid, impersonationBaseUrl }: { uid: string } & P
86
90
  useEffect(() => {
87
91
  setLoading(true);
88
92
  setError(null);
89
- getMailbox(uid)
93
+ // The administration scope: administrative metadata only - the console never shows a mailbox's mail or settings.
94
+ getMailbox(uid, { scope: "admin" })
90
95
  .then(setMailbox)
91
96
  .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this mailbox."))
92
97
  .finally(() => setLoading(false));
@@ -114,7 +119,7 @@ function MailboxDetailContent({ uid, impersonationBaseUrl }: { uid: string } & P
114
119
  <div className="flex gap-3 shrink-0">
115
120
  {mailbox.ownerUserUid && (
116
121
  <Button type="button" variant="secondary" className="!w-auto" onClick={() => setConfirmingAccess(true)}>
117
- Access this mailbox
122
+ Impersonate this user
118
123
  </Button>
119
124
  )}
120
125
  <Button
@@ -128,6 +133,13 @@ function MailboxDetailContent({ uid, impersonationBaseUrl }: { uid: string } & P
128
133
  </div>
129
134
  </div>
130
135
 
136
+ <p className="text-sm text-text-muted">
137
+ The console shows administrative details only - never a mailbox&rsquo;s mail or settings.
138
+ {mailbox.ownerUserUid
139
+ ? " To see this mailbox as its owner sees it, impersonate them: that is recorded, and you can stop at any time."
140
+ : " This shared mailbox has no owner: add yourself under Shared access to open it in the mail client."}
141
+ </p>
142
+
131
143
  <div className="bg-surface border border-border rounded-md p-6">
132
144
  <dl className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm">
133
145
  <dt className="text-text-muted">Display name</dt>
@@ -161,17 +173,18 @@ function MailboxDetailContent({ uid, impersonationBaseUrl }: { uid: string } & P
161
173
  </div>
162
174
  </div>
163
175
 
164
- <ShareAccessCard mailboxUid={mailbox.uid} ownerUserUid={mailbox.ownerUserUid} />
176
+ <ShareAccessCard mailboxUid={mailbox.uid} ownerUserUid={mailbox.ownerUserUid} currentUserUid={currentUserUid} />
165
177
 
166
178
  <EscrowScopeCard mailbox={mailbox} onUpdate={setMailbox} />
167
179
 
168
180
  {mailbox.isResource && <ResourceSettingsCard mailbox={mailbox} onUpdate={setMailbox} />}
169
181
 
170
- <Modal open={confirmingAccess} onClose={closeAccessModal} title="Access this mailbox">
182
+ <Modal open={confirmingAccess} onClose={closeAccessModal} title="Impersonate this user">
171
183
  <p className="text-sm mb-3">
172
184
  You&rsquo;ll be signed in as <strong className="break-all">{mailbox.ownerUserUid}</strong>, the owner
173
185
  of <strong className="break-all">{mailbox.primarySmtpAddress}</strong>, and see everything they can -
174
- and act as them, until you stop impersonating.
186
+ and act as them, until you stop impersonating. This is the only way an administrator sees another
187
+ user&rsquo;s mail.
175
188
  </p>
176
189
  {accessError && <Alert>{accessError}</Alert>}
177
190
  <div className="flex gap-3 justify-end mt-5">
@@ -179,7 +192,7 @@ function MailboxDetailContent({ uid, impersonationBaseUrl }: { uid: string } & P
179
192
  Cancel
180
193
  </Button>
181
194
  <Button type="button" className="!w-auto" loading={impersonating} disabled={impersonating} onClick={handleAccessMailbox}>
182
- Access mailbox
195
+ Impersonate
183
196
  </Button>
184
197
  </div>
185
198
  </Modal>
@@ -42,7 +42,8 @@ function QuarantineContent({ userUid }: { userUid?: string }) {
42
42
  function reload(uid: string) {
43
43
  setLoading(true);
44
44
  setError(null);
45
- listQuarantine(uid, { page, limit: PAGE_SIZE })
45
+ // The administration scope: any mailbox's held mail, recorded in the audit log.
46
+ listQuarantine(uid, { page, limit: PAGE_SIZE, scope: "admin" })
46
47
  .then(setEntries)
47
48
  .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load quarantine."))
48
49
  .finally(() => setLoading(false));