@rapidmx/web-client 0.10.0 → 0.12.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 (324) 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/mailboxes/new/index.tsx +3 -3
  7. package/apps/admin/quarantine/index.tsx +2 -1
  8. package/apps/admin/signing-certificates/index.tsx +343 -0
  9. package/apps/escrow/_layout.tsx +8 -2
  10. package/apps/shared/appearance/AppearanceForm.tsx +392 -0
  11. package/apps/shared/appearance/AppearanceHead.tsx +49 -0
  12. package/apps/shared/appearance/AppearanceProvider.tsx +470 -0
  13. package/apps/shared/appearance/ColorField.tsx +107 -0
  14. package/apps/shared/appearance/appearanceCache.ts +106 -0
  15. package/apps/shared/appearance/appearanceContext.ts +77 -0
  16. package/apps/shared/appearance/bootScript.ts +35 -0
  17. package/apps/shared/appearance/color.ts +169 -0
  18. package/apps/shared/appearance/photo.ts +88 -0
  19. package/apps/shared/appearance/resolvedTheme.ts +67 -0
  20. package/apps/shared/appearance/theme.ts +355 -0
  21. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +4 -4
  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 +60 -14
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +65 -16
  27. package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
  28. package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
  29. package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
  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 +25 -52
  33. package/apps/shared/components/contacts/layout/ContactsShell.tsx +10 -5
  34. package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
  35. package/apps/shared/components/layout/AppShell.tsx +98 -32
  36. package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
  37. package/apps/shared/components/layout/RailIcon.tsx +100 -0
  38. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
  39. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
  40. package/apps/shared/components/layout/UserMenu.tsx +75 -6
  41. package/apps/shared/components/mail/ConversationList.tsx +4 -2
  42. package/apps/shared/components/mail/ConversationThreadPane.tsx +487 -475
  43. package/apps/shared/components/mail/LazyReadingPane.tsx +14 -16
  44. package/apps/shared/components/mail/MailSelectionBar.tsx +2 -10
  45. package/apps/shared/components/mail/MessageDetailPane.tsx +366 -287
  46. package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
  47. package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
  48. package/apps/shared/components/mail/compose/ComposeContext.tsx +35 -2
  49. package/apps/shared/components/mail/compose/ComposeWindow.tsx +239 -437
  50. package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
  51. package/apps/shared/components/mail/layout/MailShell.tsx +39 -25
  52. package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
  53. package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
  54. package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
  55. package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
  56. package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
  57. package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
  58. package/apps/shared/components/mail/reading/color.ts +245 -0
  59. package/apps/shared/components/mail/reading/frameControl.ts +142 -0
  60. package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
  61. package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
  62. package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
  63. package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
  64. package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
  65. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
  66. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
  67. package/apps/shared/components/sharing/PrincipalListField.tsx +82 -0
  68. package/apps/shared/components/sharing/PrincipalPicker.tsx +76 -0
  69. package/apps/shared/components/sharing/PrincipalResolver.tsx +164 -0
  70. package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
  71. package/apps/shared/components/tasks/layout/TasksShell.tsx +9 -5
  72. package/apps/shared/mail/folderCounts.ts +9 -3
  73. package/apps/shared/mail/folderTree.ts +143 -0
  74. package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
  75. package/apps/shared/mail/outbox/composeBridge.ts +69 -0
  76. package/apps/shared/mail/outbox/outboxState.ts +88 -0
  77. package/apps/shared/mail/outbox/pendingSends.ts +146 -0
  78. package/apps/shared/mail/outbox/sendDecision.ts +157 -0
  79. package/apps/shared/mail/outbox/sendJob.ts +427 -0
  80. package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
  81. package/apps/shared/mail/outbox/sendState.ts +35 -0
  82. package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
  83. package/apps/shared/mail/useMailConnection.ts +81 -28
  84. package/apps/shared/mail/useMailLiveUpdates.ts +55 -10
  85. package/apps/shared/mail/useNewMailNotifications.ts +47 -32
  86. package/apps/shared/navigation/AppRouter.tsx +2 -2
  87. package/apps/shared/notifications/NotificationCenter.tsx +215 -0
  88. package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
  89. package/apps/shared/notifications/apiErrors.ts +98 -0
  90. package/apps/shared/notifications/headerOffset.ts +35 -0
  91. package/apps/shared/notifications/pushStatus.ts +56 -0
  92. package/apps/shared/notifications/store.ts +549 -0
  93. package/apps/shared/notifications/systemErrors.ts +63 -0
  94. package/apps/shared/notifications/useNotifications.ts +41 -0
  95. package/apps/shared/signing/enrollmentStorage.ts +33 -0
  96. package/apps/shared/signing/enrollmentTracker.ts +385 -0
  97. package/apps/shared/signing/enrollmentView.ts +251 -0
  98. package/apps/shared/signing/signingInfo.ts +58 -0
  99. package/apps/shared/signing/useNow.ts +19 -0
  100. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
  101. package/apps/shared/styles/app.css +80 -0
  102. package/apps/www/_layout.tsx +8 -2
  103. package/apps/www/_routes.ts +1 -0
  104. package/apps/www/calendar/index.tsx +9 -14
  105. package/apps/www/contacts/[uid].tsx +5 -8
  106. package/apps/www/contacts/index.tsx +587 -584
  107. package/apps/www/index.tsx +38 -27
  108. package/apps/www/messages/[uid].tsx +3 -1
  109. package/apps/www/settings/appearance/index.tsx +21 -0
  110. package/apps/www/settings/auto-reply/index.tsx +3 -7
  111. package/apps/www/settings/encryption/index.tsx +151 -116
  112. package/apps/www/settings/filters/[uid].tsx +3 -1
  113. package/apps/www/settings/filters/new/index.tsx +3 -1
  114. package/apps/www/settings/privacy/index.tsx +3 -8
  115. package/apps/www/settings/read-receipts/index.tsx +3 -7
  116. package/apps/www/settings/sharing/index.tsx +44 -65
  117. package/apps/www/settings/signatures/[uid].tsx +3 -1
  118. package/apps/www/settings/signatures/new/index.tsx +3 -2
  119. package/apps/www/tasks/index.tsx +11 -16
  120. package/dist/apps/admin/_layout.d.ts +5 -1
  121. package/dist/apps/admin/_layout.js +3 -2
  122. package/dist/apps/admin/index.js +3 -2
  123. package/dist/apps/admin/ingest-queue/index.js +2 -1
  124. package/dist/apps/admin/mailboxes/[uid].js +9 -6
  125. package/dist/apps/admin/mailboxes/new/index.js +1 -1
  126. package/dist/apps/admin/quarantine/index.js +2 -1
  127. package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
  128. package/dist/apps/admin/signing-certificates/index.js +161 -0
  129. package/dist/apps/escrow/_layout.d.ts +5 -1
  130. package/dist/apps/escrow/_layout.js +3 -2
  131. package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
  132. package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
  133. package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
  134. package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
  135. package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
  136. package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
  137. package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
  138. package/dist/apps/shared/appearance/ColorField.js +40 -0
  139. package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
  140. package/dist/apps/shared/appearance/appearanceCache.js +80 -0
  141. package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
  142. package/dist/apps/shared/appearance/appearanceContext.js +29 -0
  143. package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
  144. package/dist/apps/shared/appearance/bootScript.js +33 -0
  145. package/dist/apps/shared/appearance/color.d.ts +65 -0
  146. package/dist/apps/shared/appearance/color.js +132 -0
  147. package/dist/apps/shared/appearance/photo.d.ts +17 -0
  148. package/dist/apps/shared/appearance/photo.js +79 -0
  149. package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
  150. package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
  151. package/dist/apps/shared/appearance/theme.d.ts +111 -0
  152. package/dist/apps/shared/appearance/theme.js +251 -0
  153. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +3 -2
  154. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  155. package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
  156. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
  157. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
  158. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
  159. package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
  160. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +25 -5
  161. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +18 -7
  162. package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
  163. package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
  164. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
  165. package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
  166. package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
  167. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  168. package/dist/apps/shared/components/contacts/ContactsToolbar.js +24 -9
  169. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +4 -1
  170. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
  171. package/dist/apps/shared/components/layout/AppShell.d.ts +6 -1
  172. package/dist/apps/shared/components/layout/AppShell.js +59 -13
  173. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
  174. package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
  175. package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
  176. package/dist/apps/shared/components/layout/RailIcon.js +85 -0
  177. package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
  178. package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
  179. package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
  180. package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
  181. package/dist/apps/shared/components/layout/UserMenu.d.ts +7 -1
  182. package/dist/apps/shared/components/layout/UserMenu.js +41 -7
  183. package/dist/apps/shared/components/mail/ConversationList.js +3 -2
  184. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +2 -1
  185. package/dist/apps/shared/components/mail/ConversationThreadPane.js +42 -20
  186. package/dist/apps/shared/components/mail/LazyReadingPane.js +9 -8
  187. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +3 -4
  188. package/dist/apps/shared/components/mail/MailSelectionBar.js +2 -3
  189. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +18 -7
  190. package/dist/apps/shared/components/mail/MessageDetailPane.js +86 -41
  191. package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
  192. package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
  193. package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
  194. package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
  195. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +8 -0
  196. package/dist/apps/shared/components/mail/compose/ComposeContext.js +28 -3
  197. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +2 -1
  198. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +229 -328
  199. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
  200. package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
  201. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +2 -0
  202. package/dist/apps/shared/components/mail/layout/MailShell.js +20 -14
  203. package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
  204. package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
  205. package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
  206. package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
  207. package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
  208. package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
  209. package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
  210. package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
  211. package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
  212. package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
  213. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
  214. package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
  215. package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
  216. package/dist/apps/shared/components/mail/reading/color.js +201 -0
  217. package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
  218. package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
  219. package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
  220. package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
  221. package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
  222. package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
  223. package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
  224. package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
  225. package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
  226. package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
  227. package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
  228. package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
  229. package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
  230. package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
  231. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
  232. package/dist/apps/shared/components/sharing/PrincipalListField.d.ts +24 -0
  233. package/dist/apps/shared/components/sharing/PrincipalListField.js +23 -0
  234. package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +28 -0
  235. package/dist/apps/shared/components/sharing/PrincipalPicker.js +24 -0
  236. package/dist/apps/shared/components/sharing/PrincipalResolver.d.ts +44 -0
  237. package/dist/apps/shared/components/sharing/PrincipalResolver.js +92 -0
  238. package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
  239. package/dist/apps/shared/components/tasks/layout/TasksShell.js +3 -1
  240. package/dist/apps/shared/mail/folderCounts.d.ts +1 -1
  241. package/dist/apps/shared/mail/folderCounts.js +9 -3
  242. package/dist/apps/shared/mail/folderTree.d.ts +55 -0
  243. package/dist/apps/shared/mail/folderTree.js +116 -0
  244. package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
  245. package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
  246. package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
  247. package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
  248. package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
  249. package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
  250. package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
  251. package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
  252. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
  253. package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
  254. package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
  255. package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
  256. package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
  257. package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
  258. package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
  259. package/dist/apps/shared/mail/outbox/sendState.js +8 -0
  260. package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
  261. package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
  262. package/dist/apps/shared/mail/useMailConnection.d.ts +9 -8
  263. package/dist/apps/shared/mail/useMailConnection.js +64 -22
  264. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +17 -4
  265. package/dist/apps/shared/mail/useMailLiveUpdates.js +36 -10
  266. package/dist/apps/shared/mail/useNewMailNotifications.d.ts +4 -10
  267. package/dist/apps/shared/mail/useNewMailNotifications.js +43 -23
  268. package/dist/apps/shared/navigation/AppRouter.js +2 -2
  269. package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
  270. package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
  271. package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
  272. package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
  273. package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
  274. package/dist/apps/shared/notifications/apiErrors.js +86 -0
  275. package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
  276. package/dist/apps/shared/notifications/headerOffset.js +33 -0
  277. package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
  278. package/dist/apps/shared/notifications/pushStatus.js +54 -0
  279. package/dist/apps/shared/notifications/store.d.ts +136 -0
  280. package/dist/apps/shared/notifications/store.js +416 -0
  281. package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
  282. package/dist/apps/shared/notifications/systemErrors.js +58 -0
  283. package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
  284. package/dist/apps/shared/notifications/useNotifications.js +18 -0
  285. package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
  286. package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
  287. package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
  288. package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
  289. package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
  290. package/dist/apps/shared/signing/enrollmentView.js +214 -0
  291. package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
  292. package/dist/apps/shared/signing/signingInfo.js +48 -0
  293. package/dist/apps/shared/signing/useNow.d.ts +2 -0
  294. package/dist/apps/shared/signing/useNow.js +18 -0
  295. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
  296. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
  297. package/dist/apps/shared/styles/app.css +80 -0
  298. package/dist/apps/www/_layout.d.ts +5 -1
  299. package/dist/apps/www/_layout.js +3 -2
  300. package/dist/apps/www/_routes.js +1 -0
  301. package/dist/apps/www/calendar/index.js +3 -4
  302. package/dist/apps/www/contacts/[uid].js +5 -7
  303. package/dist/apps/www/contacts/index.js +14 -21
  304. package/dist/apps/www/index.js +37 -19
  305. package/dist/apps/www/messages/[uid].js +3 -1
  306. package/dist/apps/www/settings/appearance/index.d.ts +4 -0
  307. package/dist/apps/www/settings/appearance/index.js +13 -0
  308. package/dist/apps/www/settings/auto-reply/index.js +3 -6
  309. package/dist/apps/www/settings/encryption/index.js +103 -95
  310. package/dist/apps/www/settings/filters/[uid].js +3 -1
  311. package/dist/apps/www/settings/filters/new/index.js +3 -1
  312. package/dist/apps/www/settings/privacy/index.js +5 -8
  313. package/dist/apps/www/settings/read-receipts/index.js +3 -6
  314. package/dist/apps/www/settings/sharing/index.js +19 -33
  315. package/dist/apps/www/settings/signatures/[uid].js +3 -1
  316. package/dist/apps/www/settings/signatures/new/index.js +3 -2
  317. package/dist/apps/www/tasks/index.js +8 -14
  318. package/package.json +2 -2
  319. package/apps/shared/components/mail/NewMailToasts.tsx +0 -141
  320. package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
  321. package/dist/apps/shared/components/mail/NewMailToasts.d.ts +0 -19
  322. package/dist/apps/shared/components/mail/NewMailToasts.js +0 -55
  323. package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
  324. package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
@@ -2,68 +2,69 @@
2
2
  // Copyright (C) 2026 Jean-Philippe Steinmetz
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
- import React, { FormEvent, useEffect, useState } from "react";
5
+ import React, { useEffect, useState } from "react";
6
6
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
- import { AccessControlList, getMailboxAcl, grantMailboxAccess, revokeMailboxAccess } from "@rapidmx/react-shared/mail/mailApi.js";
7
+ import {
8
+ listMailboxAccess,
9
+ MailboxAccessMember,
10
+ MailboxAccessRole,
11
+ removeMailboxAccess,
12
+ setMailboxAccess,
13
+ } from "@rapidmx/react-shared/mail/mailboxAccessApi.js";
8
14
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
9
15
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
10
16
  import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
17
+ import PrincipalPicker from "../../sharing/PrincipalPicker.js";
11
18
 
12
- const DEFAULT_DELEGATE_ACTIONS = ["read", "list", "count", "exists"];
19
+ const ROLE_LABELS: Record<MailboxAccessRole, string> = { viewer: "Read only", manager: "Full access" };
13
20
 
14
21
  export interface ShareAccessCardProps {
15
22
  mailboxUid: string;
16
- /** The mailbox's owner - their own grant is shown but can't be revoked from here. */
23
+ /** The mailbox's owner: with one, only the owner can grant access to it. Without, the mailbox is shared and an
24
+ * administrator grants access here - to themselves included. */
17
25
  ownerUserUid?: string;
26
+ /** The signed-in administrator, for "Add me" on a shared mailbox. */
27
+ currentUserUid?: string;
28
+ }
29
+
30
+ function roleLabel(member: MailboxAccessMember): string {
31
+ if (member.role === "viewer") return ROLE_LABELS.viewer;
32
+ if (member.role === "manager") return ROLE_LABELS.manager;
33
+ return (member.actions ?? []).join(", ");
18
34
  }
19
35
 
20
36
  /**
21
- * Lets an admin (or, eventually, the mailbox owner) view and edit a mailbox's delegate access — the
22
- * mechanism behind Exchange-style shared mailboxes. `records` on a mailbox's own ACL doubles as both the
23
- * owner's grant (if any) and every delegate's — see `BaseMailboxRoute`'s doc comment in `@rapidmx/restapi`.
37
+ * A mailbox's members - the mechanism behind Exchange-style shared mailboxes - through the Sharing endpoints
38
+ * (`/mail/mailboxes/:id/access`), the audited way an administrator reaches a mailbox they hold no grant on. An
39
+ * administrator can review any mailbox's members and revoke any of them; they can grant access only on a shared
40
+ * (ownerless) mailbox - themselves included, which is how it appears in their own mail client. A personal mailbox is
41
+ * shared by its owner (or by an administrator impersonating them).
24
42
  */
25
- export default function ShareAccessCard({ mailboxUid, ownerUserUid }: ShareAccessCardProps) {
26
- const [acl, setAcl] = useState<AccessControlList | null>(null);
43
+ export default function ShareAccessCard({ mailboxUid, ownerUserUid, currentUserUid }: ShareAccessCardProps) {
44
+ const [members, setMembers] = useState<MailboxAccessMember[]>([]);
27
45
  const [loading, setLoading] = useState(true);
28
46
  const [error, setError] = useState<string | null>(null);
29
- const [notice, setNotice] = useState<string | null>(null);
30
- const [newUserUid, setNewUserUid] = useState("");
31
47
  const [saving, setSaving] = useState(false);
32
48
  const [revokeTarget, setRevokeTarget] = useState<string | null>(null);
49
+ // An entry that is not a user uid, being replaced with the user it was meant for.
50
+ const [replacing, setReplacing] = useState<MailboxAccessMember | null>(null);
33
51
 
34
52
  function reload() {
35
53
  setLoading(true);
36
54
  setError(null);
37
- getMailboxAcl(mailboxUid)
38
- .then(setAcl)
55
+ listMailboxAccess(mailboxUid)
56
+ .then(setMembers)
39
57
  .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load share settings."))
40
58
  .finally(() => setLoading(false));
41
59
  }
42
60
 
43
61
  useEffect(reload, [mailboxUid]);
44
62
 
45
- async function handleGrant(e: FormEvent) {
46
- e.preventDefault();
47
- const userUid = newUserUid.trim();
48
- if (!userUid) {
49
- return;
50
- }
63
+ async function addMe(userUid: string) {
51
64
  setSaving(true);
52
65
  setError(null);
53
- setNotice(null);
54
66
  try {
55
- // `grantMailboxAccess()` replaces any existing record for this uid, so granting read access to someone
56
- // who already has more (e.g. the owner) would silently downgrade them. Merge with their current actions.
57
- const current = await getMailboxAcl(mailboxUid);
58
- const existing = current.records.find((record) => record.userOrRoleId === userUid)?.actions ?? [];
59
- const missing = DEFAULT_DELEGATE_ACTIONS.filter((action) => !existing.includes(action));
60
- if (missing.length === 0) {
61
- setNotice(`${userUid} already has this access.`);
62
- setNewUserUid("");
63
- return;
64
- }
65
- await grantMailboxAccess(mailboxUid, userUid, [...existing, ...missing]);
66
- setNewUserUid("");
67
+ await setMailboxAccess(mailboxUid, userUid, "manager");
67
68
  reload();
68
69
  } catch (err) {
69
70
  setError(err instanceof ApiRequestError ? err.message : "Could not grant access.");
@@ -72,15 +73,33 @@ export default function ShareAccessCard({ mailboxUid, ownerUserUid }: ShareAcces
72
73
  }
73
74
  }
74
75
 
76
+ // Once the person is granted, the entry they replace (if any) is removed: the string it named never matched anyone.
77
+ async function handleGranted() {
78
+ const stale = replacing;
79
+ setReplacing(null);
80
+ let failure: string | null = null;
81
+ if (stale) {
82
+ try {
83
+ await removeMailboxAccess(mailboxUid, stale.userOrRoleId);
84
+ } catch (err) {
85
+ failure = err instanceof ApiRequestError ? err.message : "Could not remove the old entry.";
86
+ }
87
+ }
88
+ reload();
89
+ // After the reload, which clears the error it started with.
90
+ if (failure) {
91
+ setError(failure);
92
+ }
93
+ }
94
+
75
95
  // Only ever invoked from the revoke-confirmation modal below, which only renders once `revokeTarget` is set.
76
96
  async function handleRevoke() {
77
97
  const userOrRoleId = revokeTarget!;
78
98
  setRevokeTarget(null);
79
99
  setSaving(true);
80
100
  setError(null);
81
- setNotice(null);
82
101
  try {
83
- await revokeMailboxAccess(mailboxUid, userOrRoleId);
102
+ await removeMailboxAccess(mailboxUid, userOrRoleId);
84
103
  reload();
85
104
  } catch (err) {
86
105
  setError(err instanceof ApiRequestError ? err.message : "Could not revoke access.");
@@ -89,61 +108,89 @@ export default function ShareAccessCard({ mailboxUid, ownerUserUid }: ShareAcces
89
108
  }
90
109
  }
91
110
 
111
+ const shared = !ownerUserUid;
112
+ const isMember = members.some((member) => member.userOrRoleId === currentUserUid);
113
+
92
114
  return (
93
115
  <div className="bg-surface border border-border rounded-md p-6">
94
116
  <h2 className="text-base font-bold uppercase tracking-wide mb-1">Shared access</h2>
95
117
  <p className="text-sm text-text-muted mb-4">
96
- Grants read access to this mailbox for another user, independent of ownership.
118
+ {shared
119
+ ? "Who has access to this shared mailbox. Adding yourself is how it appears in your own mail client; every change is recorded in the audit log."
120
+ : "Who the owner has shared this mailbox with. You can review and revoke access; only the owner can grant it (impersonate them to do so)."}
97
121
  </p>
98
122
 
99
123
  {error && <Alert>{error}</Alert>}
100
- {notice && !error && <div className="mb-4 text-sm text-text-muted">{notice}</div>}
101
124
 
102
125
  {loading ? (
103
126
  <p className="text-sm text-text-muted">Loading&hellip;</p>
104
127
  ) : (
105
128
  <ul className="flex flex-col gap-2 mb-4">
106
- {(acl?.records ?? []).length === 0 && (
107
- <li className="text-sm text-text-muted">No grants on this mailbox yet.</li>
108
- )}
109
- {acl?.records.map((record) => (
129
+ {members.length === 0 && <li className="text-sm text-text-muted">No grants on this mailbox yet.</li>}
130
+ {members.map((member) => (
110
131
  <li
111
- key={record.userOrRoleId}
132
+ key={member.userOrRoleId}
112
133
  className="flex items-center justify-between gap-3 py-2 px-3 border border-border rounded-sm"
113
134
  >
114
135
  <div>
115
- <div className="text-sm font-medium">{record.userOrRoleId}</div>
116
- <div className="text-xs text-text-muted">{record.actions.join(", ")}</div>
136
+ <div className="text-sm font-medium">{member.userOrRoleId}</div>
137
+ <div className="text-xs text-text-muted">{roleLabel(member)}</div>
138
+ {member.noEffect && (
139
+ <div className="text-xs text-danger mt-0.5">
140
+ Not a user - this entry has no effect
141
+ {shared ? " - replace it with the user it was meant for or remove it." : " - remove it."}
142
+ </div>
143
+ )}
117
144
  </div>
118
- {record.userOrRoleId === ownerUserUid ? (
119
- <span className="text-xs font-bold uppercase tracking-wide text-text-muted">Owner</span>
120
- ) : (
145
+ <span className="flex items-center gap-3 shrink-0">
146
+ {member.noEffect && shared && (
147
+ <button
148
+ type="button"
149
+ onClick={() => setReplacing(member)}
150
+ disabled={saving}
151
+ className="text-sm text-primary-dark hover:underline disabled:opacity-55"
152
+ >
153
+ Replace with a user
154
+ </button>
155
+ )}
121
156
  <button
122
157
  type="button"
123
- onClick={() => setRevokeTarget(record.userOrRoleId)}
158
+ onClick={() => setRevokeTarget(member.userOrRoleId)}
124
159
  disabled={saving}
125
160
  className="text-sm text-danger hover:underline disabled:opacity-55"
126
161
  >
127
162
  Revoke
128
163
  </button>
129
- )}
164
+ </span>
130
165
  </li>
131
166
  ))}
132
167
  </ul>
133
168
  )}
134
169
 
135
- <form onSubmit={handleGrant} className="flex gap-2">
136
- <input
137
- type="text"
138
- value={newUserUid}
139
- onChange={(e) => setNewUserUid(e.target.value)}
140
- placeholder="User uid to grant access to"
141
- className="flex-1 text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary"
142
- />
143
- <Button type="submit" loading={saving} disabled={saving} className="!w-auto">
144
- Grant
145
- </Button>
146
- </form>
170
+ {shared && (
171
+ <>
172
+ {currentUserUid && !isMember && !loading && (
173
+ <Button
174
+ type="button"
175
+ variant="secondary"
176
+ className="!w-auto mb-3"
177
+ disabled={saving}
178
+ onClick={() => void addMe(currentUserUid)}
179
+ >
180
+ Add me
181
+ </Button>
182
+ )}
183
+ <PrincipalPicker
184
+ key={replacing?.userOrRoleId ?? "new"}
185
+ mailboxUid={mailboxUid}
186
+ roleLabels={ROLE_LABELS}
187
+ initialPrincipal={replacing?.userOrRoleId}
188
+ defaultRole={replacing?.role === "manager" ? "manager" : "viewer"}
189
+ onGranted={handleGranted}
190
+ onCancel={replacing ? () => setReplacing(null) : undefined}
191
+ />
192
+ </>
193
+ )}
147
194
 
148
195
  <Modal open={revokeTarget !== null} onClose={() => setRevokeTarget(null)} title="Revoke access">
149
196
  <p className="text-sm mb-5">
@@ -16,12 +16,19 @@ import {
16
16
  } from "@rapidmx/react-shared/branding/brandingApi.js";
17
17
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
18
18
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
19
+ import CopyButton from "@rapidmx/react-shared/components/buttons/CopyButton.js";
19
20
 
20
21
  const INPUT_CLASS =
21
22
  "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
22
23
 
23
24
  const MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
24
25
 
26
+ /** A header that uses both variables - the snippet the Branding page offers to copy. */
27
+ export const HEADER_HTML_EXAMPLE = `<div style="display:flex; align-items:center; justify-content:space-between; padding:0.5rem 1rem">
28
+ <span>{APP_TITLE}</span>
29
+ <span>{USER_MENU}</span>
30
+ </div>`;
31
+
25
32
  /** SVG logo/icon uploads are refused (the server refuses them too): an SVG served from this origin can carry script. */
26
33
  export function isSvgFile(file: File): boolean {
27
34
  return file.type === "image/svg+xml" || file.name.toLowerCase().endsWith(".svg");
@@ -376,7 +383,21 @@ export default function BrandingForm({ branding, onChange, embedded = false }: B
376
383
  value={headerHtml}
377
384
  onChange={(e) => setHeaderHtml(e.target.value)}
378
385
  />
386
+ <span className="text-xs text-text-muted">
387
+ A header replaces the app's own title bar and the icon at the top of the icon rail, and stays at the top of the window. Write{" "}
388
+ <code>{"{USER_MENU}"}</code> where the account menu (avatar, Settings, Sign Out) should go, and <code>{"{APP_TITLE}"}</code>{" "}
389
+ where the name of the app on screen should show. Both work in the footer too. <code>{"{USER_MENU}"}</code> is replaced once
390
+ (the header's, else the footer's); without it the menu sits in a small cell at the right end of the header, so it is never lost.
391
+ Put it where your styles don't hide it on a phone.
392
+ </span>
379
393
  </label>
394
+ <div className="flex flex-col gap-1.5 text-sm" role="group" aria-label="Header example">
395
+ <div className="flex items-center justify-between gap-2">
396
+ <span className="font-semibold">Example header</span>
397
+ <CopyButton value={HEADER_HTML_EXAMPLE} label="Copy the example header HTML" />
398
+ </div>
399
+ <pre className="overflow-x-auto rounded-sm border border-border bg-surface-alt p-3 font-mono text-xs text-text">{HEADER_HTML_EXAMPLE}</pre>
400
+ </div>
380
401
  <label className="flex flex-col gap-1.5 text-sm">
381
402
  <span className="font-semibold">Footer HTML (optional)</span>
382
403
  <textarea
@@ -386,6 +407,9 @@ export default function BrandingForm({ branding, onChange, embedded = false }: B
386
407
  value={footerHtml}
387
408
  onChange={(e) => setFooterHtml(e.target.value)}
388
409
  />
410
+ <span className="text-xs text-text-muted">
411
+ <code>{"{APP_TITLE}"}</code> works here as well, and <code>{"{USER_MENU}"}</code> takes the account menu (opening upward) when the header has none.
412
+ </span>
389
413
  </label>
390
414
  <div>
391
415
  <Button type="submit" loading={saving} disabled={saving} className="!w-auto">
@@ -15,6 +15,8 @@ const RECORD_TYPE_LABELS: Record<DnsRecordCheck["type"], string> = {
15
15
  spf: "SPF",
16
16
  dkim: "DKIM",
17
17
  dmarc: "DMARC",
18
+ autodiscover_cname: "Autodiscover (CNAME)",
19
+ autodiscover_srv: "Autodiscover (SRV)",
18
20
  };
19
21
 
20
22
  /** How each record is named in a Copy button's accessible name ("Copy value for the SPF record"). */
@@ -24,6 +26,17 @@ const RECORD_TYPE_NAMES: Record<DnsRecordCheck["type"], string> = {
24
26
  spf: "SPF",
25
27
  dkim: "DKIM",
26
28
  dmarc: "DMARC",
29
+ autodiscover_cname: "Autodiscover CNAME",
30
+ autodiscover_srv: "Autodiscover SRV",
31
+ };
32
+
33
+ /** A one-line "why this matters", shown under a checklist row's label - only for record types unfamiliar
34
+ * enough to need one; the well-known mail records (ownership/MX/SPF/DKIM/DMARC) don't. */
35
+ const RECORD_TYPE_HELP: Partial<Record<DnsRecordCheck["type"], string>> = {
36
+ autodiscover_cname:
37
+ "So EAS and Outlook clients can find this server automatically from just an email address. This hostname also needs to be covered by this server's TLS certificate.",
38
+ autodiscover_srv:
39
+ "Same purpose as the CNAME above, for clients that check DNS SRV instead - and, unlike the CNAME, needs no extra TLS certificate, since it points at a hostname already covered. Prefer this one when a second certificate is hard to obtain.",
27
40
  };
28
41
 
29
42
  /** An MX record's recommended value is `"<priority> <mail server>"` - two separate fields in a DNS provider's form. */
@@ -32,6 +45,12 @@ function splitMxValue(value: string): { priority: string; server: string } | nul
32
45
  return match ? { priority: match[1], server: match[2] } : null;
33
46
  }
34
47
 
48
+ /** A SRV record's recommended value is `"<priority> <weight> <port> <target>"` - four separate fields in a DNS provider's form. */
49
+ function splitSrvValue(value: string): { priority: string; weight: string; port: string; target: string } | null {
50
+ const match = /^(\d+)\s+(\d+)\s+(\d+)\s+(\S+)$/.exec(value.trim());
51
+ return match ? { priority: match[1], weight: match[2], port: match[3], target: match[4] } : null;
52
+ }
53
+
35
54
  /** A value to type into a DNS provider's form, shown in full (long DKIM keys wrap) with a Copy button beside it. */
36
55
  function CopyableValue({ value, copyLabel }: { value: string; copyLabel: string }) {
37
56
  return (
@@ -181,6 +200,9 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
181
200
  <td className="py-2.5 px-2.5 border-b border-border">
182
201
  {RECORD_TYPE_LABELS[check.type]}
183
202
  <div className="text-xs text-text-muted">Type: {check.recordKind}</div>
203
+ {RECORD_TYPE_HELP[check.type] && (
204
+ <div className="text-xs text-text-muted mt-1 max-w-xs">{RECORD_TYPE_HELP[check.type]}</div>
205
+ )}
184
206
  </td>
185
207
  <td className="py-2.5 px-2.5 border-b border-border">
186
208
  {!check.configured ? (
@@ -228,23 +250,47 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
228
250
  );
229
251
  }
230
252
 
231
- /** The value cell of a checklist row: an MX value as its two form fields (priority and mail server), anything else whole. */
253
+ /** The value cell of a checklist row: an MX value as its two form fields (priority and mail server), a SRV value as
254
+ * its four form fields (priority, weight, port and target), anything else whole. */
232
255
  function DnsRecordValue({ check, value }: { check: DnsRecordCheck; value: string }) {
233
256
  const recordName = RECORD_TYPE_NAMES[check.type];
234
257
  const mx = check.type === "mx" ? splitMxValue(value) : null;
235
- if (!mx) {
236
- return <CopyableValue value={value} copyLabel={`Copy value for the ${recordName} record`} />;
237
- }
238
- return (
239
- <div className="flex flex-col gap-1.5">
240
- <div className="flex items-start gap-2">
241
- <span className="text-xs w-20 shrink-0 pt-1">Priority</span>
242
- <CopyableValue value={mx.priority} copyLabel={`Copy priority for the ${recordName} record`} />
258
+ if (mx) {
259
+ return (
260
+ <div className="flex flex-col gap-1.5">
261
+ <div className="flex items-start gap-2">
262
+ <span className="text-xs w-20 shrink-0 pt-1">Priority</span>
263
+ <CopyableValue value={mx.priority} copyLabel={`Copy priority for the ${recordName} record`} />
264
+ </div>
265
+ <div className="flex items-start gap-2">
266
+ <span className="text-xs w-20 shrink-0 pt-1">Mail server</span>
267
+ <CopyableValue value={mx.server} copyLabel={`Copy mail server for the ${recordName} record`} />
268
+ </div>
243
269
  </div>
244
- <div className="flex items-start gap-2">
245
- <span className="text-xs w-20 shrink-0 pt-1">Mail server</span>
246
- <CopyableValue value={mx.server} copyLabel={`Copy mail server for the ${recordName} record`} />
270
+ );
271
+ }
272
+ const srv = check.type === "autodiscover_srv" ? splitSrvValue(value) : null;
273
+ if (srv) {
274
+ return (
275
+ <div className="flex flex-col gap-1.5">
276
+ <div className="flex items-start gap-2">
277
+ <span className="text-xs w-20 shrink-0 pt-1">Priority</span>
278
+ <CopyableValue value={srv.priority} copyLabel={`Copy priority for the ${recordName} record`} />
279
+ </div>
280
+ <div className="flex items-start gap-2">
281
+ <span className="text-xs w-20 shrink-0 pt-1">Weight</span>
282
+ <CopyableValue value={srv.weight} copyLabel={`Copy weight for the ${recordName} record`} />
283
+ </div>
284
+ <div className="flex items-start gap-2">
285
+ <span className="text-xs w-20 shrink-0 pt-1">Port</span>
286
+ <CopyableValue value={srv.port} copyLabel={`Copy port for the ${recordName} record`} />
287
+ </div>
288
+ <div className="flex items-start gap-2">
289
+ <span className="text-xs w-20 shrink-0 pt-1">Target</span>
290
+ <CopyableValue value={srv.target} copyLabel={`Copy target for the ${recordName} record`} />
291
+ </div>
247
292
  </div>
248
- </div>
249
- );
293
+ );
294
+ }
295
+ return <CopyableValue value={value} copyLabel={`Copy value for the ${recordName} record`} />;
250
296
  }
@@ -5,10 +5,11 @@
5
5
  import React, { FormEvent, useEffect, useRef, useState } from "react";
6
6
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
7
  import { getMailboxPolicy } from "@rapidmx/react-shared/admin/mailboxPolicyApi.js";
8
- import { createMailbox, listMailboxDomains, Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
8
+ import { createMailbox, listMailboxDomains, Mailbox, resolveMailboxOwner, ResolvedPrincipal } from "@rapidmx/react-shared/mail/mailApi.js";
9
9
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
10
10
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
11
11
  import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
12
+ import PrincipalResolver, { describePerson } from "../../sharing/PrincipalResolver.js";
12
13
 
13
14
  const INPUT_CLASS =
14
15
  "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
@@ -33,7 +34,8 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
33
34
  const [domains, setDomains] = useState<string[]>([]);
34
35
  const [domain, setDomain] = useState("");
35
36
  const [displayName, setDisplayName] = useState(defaults?.displayName ?? "");
36
- const [ownerUserUid, setOwnerUserUid] = useState(defaults?.ownerUserUid ?? "");
37
+ const [ownerMode, setOwnerMode] = useState<"shared" | "owned">(defaults?.ownerUserUid ? "owned" : "shared");
38
+ const [resolvedOwner, setResolvedOwner] = useState<ResolvedPrincipal | null>(null);
37
39
  const [timezone, setTimezone] = useState("UTC");
38
40
  const [quotaGb, setQuotaGb] = useState(5);
39
41
  // Only replaced by the mailbox policy's default if the admin hasn't typed a quota of their own yet.
@@ -90,16 +92,19 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
90
92
  setError("A display name can't contain \"@\" (or a look-alike) or line breaks.");
91
93
  return;
92
94
  }
95
+ if (ownerMode === "owned" && !resolvedOwner) {
96
+ setError("Look up and confirm the mailbox's owner, or choose a shared mailbox instead.");
97
+ return;
98
+ }
93
99
 
94
100
  setSaving(true);
95
101
  try {
96
- // ownerUserUid left blank creates a true ownerless shared mailbox (e.g. support@example.com);
97
- // delegates are then granted access from the mailbox's detail page. A resource mailbox is
98
- // ownerless the same way — leave "Owner user uid" blank for one too.
102
+ // "Shared mailbox" creates a true ownerless mailbox (e.g. support@example.com); delegates are then
103
+ // granted access from the mailbox's detail page. A resource mailbox is ownerless the same way.
99
104
  const mailbox = await createMailbox({
100
105
  primarySmtpAddress: address,
101
106
  displayName: displayName.trim(),
102
- ownerUserUid: ownerUserUid.trim() || undefined,
107
+ ownerUserUid: ownerMode === "owned" ? resolvedOwner!.userUid : undefined,
103
108
  timezone,
104
109
  quotaBytes: Math.round(quotaGb * 1_000_000_000),
105
110
  ...(isResource && {
@@ -175,16 +180,60 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
175
180
  />
176
181
  </FormField>
177
182
 
178
- <FormField label="Owner user uid (optional)" htmlFor="ownerUserUid">
179
- <input
180
- id="ownerUserUid"
181
- type="text"
182
- className={INPUT_CLASS}
183
- value={ownerUserUid}
184
- onChange={(e) => setOwnerUserUid(e.target.value)}
185
- placeholder="Leave blank for a shared mailbox"
186
- />
187
- </FormField>
183
+ <div className="mb-4">
184
+ <span className="block text-sm font-semibold mb-1.5 text-text">Owner</span>
185
+ <div className="flex flex-col gap-2">
186
+ <label className="flex items-center gap-2 text-sm">
187
+ <input
188
+ type="radio"
189
+ name="ownerMode"
190
+ checked={ownerMode === "shared"}
191
+ onChange={() => {
192
+ setOwnerMode("shared");
193
+ setResolvedOwner(null);
194
+ }}
195
+ />
196
+ Shared mailbox (no single owner)
197
+ </label>
198
+ <label className="flex items-center gap-2 text-sm">
199
+ <input
200
+ type="radio"
201
+ name="ownerMode"
202
+ checked={ownerMode === "owned"}
203
+ onChange={() => setOwnerMode("owned")}
204
+ />
205
+ Owned by a specific person
206
+ </label>
207
+ {ownerMode === "owned" &&
208
+ (resolvedOwner ? (
209
+ <div className="flex items-center justify-between gap-3 text-sm py-2 px-3 border border-border rounded-sm">
210
+ <span>
211
+ Owner: <strong className="break-all">{describePerson(resolvedOwner)}</strong>
212
+ </span>
213
+ <Button type="button" variant="text" className="!w-auto" onClick={() => setResolvedOwner(null)}>
214
+ Change
215
+ </Button>
216
+ </div>
217
+ ) : (
218
+ <PrincipalResolver
219
+ resolve={resolveMailboxOwner}
220
+ onResolved={(person) => {
221
+ setResolvedOwner(person);
222
+ }}
223
+ initialPrincipal={defaults?.ownerUserUid}
224
+ placeholder="Email address, username or user id"
225
+ ariaLabel="Mailbox owner"
226
+ confirmLabel="Use this owner"
227
+ confirmErrorMessage="Could not confirm this person."
228
+ describeConfirm={(person) => (
229
+ <>
230
+ Make <strong className="break-all">{describePerson(person)}</strong> the owner?
231
+ </>
232
+ )}
233
+ />
234
+ ))}
235
+ </div>
236
+ </div>
188
237
 
189
238
  <FormField label="Timezone" htmlFor="timezone">
190
239
  <input