@rapidmx/web-client 0.4.0 → 0.6.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 (320) hide show
  1. package/README.md +88 -0
  2. package/apps/admin/audit-log/index.tsx +35 -8
  3. package/apps/admin/data-requests/index.tsx +480 -485
  4. package/apps/admin/distribution-lists/[uid].tsx +19 -1
  5. package/apps/admin/escrow-scopes/[uid].tsx +348 -237
  6. package/apps/admin/escrow-scopes/new/index.tsx +8 -2
  7. package/apps/admin/index.tsx +19 -1
  8. package/apps/admin/ingest-queue/index.tsx +44 -5
  9. package/apps/admin/mailboxes/[uid].tsx +211 -184
  10. package/apps/admin/quarantine/index.tsx +46 -14
  11. package/apps/admin/transport-rules/[uid].tsx +30 -1
  12. package/apps/admin/transport-rules/_transportRuleConfig.tsx +38 -2
  13. package/apps/admin/transport-rules/index.tsx +24 -6
  14. package/apps/admin/transport-rules/new/index.tsx +30 -1
  15. package/apps/escrow/_layout.tsx +2 -3
  16. package/apps/escrow/audit-log/index.tsx +196 -168
  17. package/apps/escrow/matters/[uid].tsx +621 -521
  18. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +22 -1
  19. package/apps/shared/components/admin/layout/AdminShell.tsx +28 -9
  20. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -0
  21. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  22. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +55 -11
  23. package/apps/shared/components/admin/settings/BrandingForm.tsx +15 -4
  24. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +18 -7
  25. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +10 -2
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +6 -0
  27. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +45 -12
  28. package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -595
  29. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -98
  30. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +49 -13
  31. package/apps/shared/components/admin/setup/SetupWizard.tsx +150 -34
  32. package/apps/shared/components/admin/signOut.ts +79 -0
  33. package/apps/shared/components/admin/usePagedList.tsx +129 -0
  34. package/apps/shared/components/calendar/EventModal.tsx +717 -549
  35. package/apps/shared/components/calendar/MonthView.tsx +173 -139
  36. package/apps/shared/components/calendar/RecurrenceEditor.tsx +222 -180
  37. package/apps/shared/components/calendar/SplitDayView.tsx +143 -137
  38. package/apps/shared/components/calendar/TimeGridView.tsx +244 -198
  39. package/apps/shared/components/calendar/allDay.ts +124 -0
  40. package/apps/shared/components/calendar/layout/CalendarShell.tsx +194 -192
  41. package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
  42. package/apps/shared/components/contacts/ContactForm.tsx +377 -348
  43. package/apps/shared/components/contacts/ContactsSidebar.tsx +6 -2
  44. package/apps/shared/components/contacts/KeyChangeReview.tsx +192 -0
  45. package/apps/shared/components/contacts/contactKeys.ts +78 -0
  46. package/apps/shared/components/contacts/layout/ContactsShell.tsx +199 -197
  47. package/apps/shared/components/escrow/layout/EscrowShell.tsx +148 -134
  48. package/apps/shared/components/layout/AppShell.tsx +123 -33
  49. package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
  50. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +389 -298
  51. package/apps/shared/components/layout/MailboxProvisioning.tsx +19 -3
  52. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +350 -0
  53. package/apps/shared/components/layout/UnlockPromptProvider.tsx +257 -128
  54. package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
  55. package/apps/shared/components/mail/MessageDetailPane.tsx +1318 -630
  56. package/apps/shared/components/mail/compose/ComposeContext.tsx +20 -15
  57. package/apps/shared/components/mail/compose/ComposeWindow.tsx +854 -132
  58. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
  59. package/apps/shared/components/mail/layout/MailShell.tsx +420 -408
  60. package/apps/shared/components/mail/pinnedSigners.ts +124 -0
  61. package/apps/shared/components/mail/verificationSeals.ts +125 -0
  62. package/apps/shared/components/mail/writableMailboxes.ts +101 -0
  63. package/apps/shared/components/rules/RuleBuilder.tsx +311 -276
  64. package/apps/shared/components/settings/layout/SettingsShell.tsx +23 -9
  65. package/apps/shared/components/tasks/layout/TasksShell.tsx +201 -199
  66. package/apps/shared/mail/listAllPages.ts +39 -0
  67. package/apps/shared/plugins/pluginNav.ts +73 -0
  68. package/apps/shared/search/LocalIndexLifecycle.tsx +64 -55
  69. package/apps/shared/search/localIndexBuilder.ts +481 -179
  70. package/apps/shared/search/localIndexRpcClient.ts +231 -29
  71. package/apps/shared/search/localIndexSchema.ts +15 -6
  72. package/apps/shared/search/localIndexStorage.ts +98 -0
  73. package/apps/shared/search/localIndexVFS.ts +32 -1
  74. package/apps/shared/search/localIndexWorker.ts +648 -186
  75. package/apps/www/calendar/index.tsx +438 -417
  76. package/apps/www/contacts/[uid].tsx +21 -2
  77. package/apps/www/contacts/index.tsx +116 -12
  78. package/apps/www/index.tsx +1314 -1041
  79. package/apps/www/messages/[uid].tsx +18 -6
  80. package/apps/www/settings/auto-reply/index.tsx +134 -129
  81. package/apps/www/settings/encryption/index.tsx +739 -216
  82. package/apps/www/settings/filters/[uid].tsx +171 -155
  83. package/apps/www/settings/filters/new/index.tsx +8 -1
  84. package/apps/www/settings/focused-inbox/index.tsx +150 -148
  85. package/apps/www/settings/privacy/index.tsx +494 -436
  86. package/apps/www/settings/read-receipts/index.tsx +148 -125
  87. package/apps/www/settings/sharing/index.tsx +17 -14
  88. package/apps/www/tasks/index.tsx +638 -579
  89. package/dist/apps/admin/_404.d.ts +2 -0
  90. package/dist/apps/admin/_500.d.ts +2 -0
  91. package/dist/apps/admin/_layout.d.ts +8 -0
  92. package/dist/apps/admin/audit-log/index.d.ts +11 -0
  93. package/dist/apps/admin/audit-log/index.js +38 -8
  94. package/dist/apps/admin/branding/index.d.ts +3 -0
  95. package/dist/apps/admin/data-requests/index.d.ts +3 -0
  96. package/dist/apps/admin/data-requests/index.js +52 -72
  97. package/dist/apps/admin/distribution-lists/[uid].d.ts +7 -0
  98. package/dist/apps/admin/distribution-lists/[uid].js +1 -1
  99. package/dist/apps/admin/distribution-lists/index.d.ts +3 -0
  100. package/dist/apps/admin/distribution-lists/new/index.d.ts +3 -0
  101. package/dist/apps/admin/domains/[uid].d.ts +7 -0
  102. package/dist/apps/admin/domains/index.d.ts +3 -0
  103. package/dist/apps/admin/domains/new/index.d.ts +3 -0
  104. package/dist/apps/admin/encryption-policy/index.d.ts +3 -0
  105. package/dist/apps/admin/escrow-scopes/[uid].d.ts +21 -0
  106. package/dist/apps/admin/escrow-scopes/[uid].js +91 -12
  107. package/dist/apps/admin/escrow-scopes/index.d.ts +3 -0
  108. package/dist/apps/admin/escrow-scopes/new/index.d.ts +3 -0
  109. package/dist/apps/admin/escrow-scopes/new/index.js +8 -3
  110. package/dist/apps/admin/index.d.ts +3 -0
  111. package/dist/apps/admin/index.js +4 -1
  112. package/dist/apps/admin/ingest-queue/index.d.ts +4 -0
  113. package/dist/apps/admin/ingest-queue/index.js +24 -6
  114. package/dist/apps/admin/mailbox-policy/index.d.ts +3 -0
  115. package/dist/apps/admin/mailboxes/[uid].d.ts +7 -0
  116. package/dist/apps/admin/mailboxes/[uid].js +20 -4
  117. package/dist/apps/admin/mailboxes/new/index.d.ts +3 -0
  118. package/dist/apps/admin/plugins/index.d.ts +3 -0
  119. package/dist/apps/admin/quarantine/index.d.ts +6 -0
  120. package/dist/apps/admin/quarantine/index.js +20 -11
  121. package/dist/apps/admin/retention-policy/index.d.ts +3 -0
  122. package/dist/apps/admin/setup/index.d.ts +3 -0
  123. package/dist/apps/admin/transport-rules/[uid].d.ts +7 -0
  124. package/dist/apps/admin/transport-rules/[uid].js +18 -2
  125. package/dist/apps/admin/transport-rules/_transportRuleConfig.d.ts +21 -0
  126. package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
  127. package/dist/apps/admin/transport-rules/index.d.ts +3 -0
  128. package/dist/apps/admin/transport-rules/index.js +22 -6
  129. package/dist/apps/admin/transport-rules/new/index.d.ts +3 -0
  130. package/dist/apps/admin/transport-rules/new/index.js +18 -2
  131. package/dist/apps/escrow/_layout.d.ts +10 -0
  132. package/dist/apps/escrow/_layout.js +3 -3
  133. package/dist/apps/escrow/audit-log/index.d.ts +9 -0
  134. package/dist/apps/escrow/audit-log/index.js +26 -1
  135. package/dist/apps/escrow/index.d.ts +3 -0
  136. package/dist/apps/escrow/matters/[uid].d.ts +7 -0
  137. package/dist/apps/escrow/matters/[uid].js +77 -42
  138. package/dist/apps/escrow/matters/new/index.d.ts +3 -0
  139. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.d.ts +17 -0
  140. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +32 -0
  141. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
  142. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +32 -0
  143. package/dist/apps/shared/components/admin/layout/AdminShell.js +15 -6
  144. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.d.ts +12 -0
  145. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -0
  146. package/dist/apps/shared/components/admin/mailboxes/MailboxTable.d.ts +6 -0
  147. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.d.ts +19 -0
  148. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  149. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +12 -0
  150. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
  151. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +9 -0
  152. package/dist/apps/shared/components/admin/settings/BrandingForm.js +15 -4
  153. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +12 -0
  154. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +13 -7
  155. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.d.ts +12 -0
  156. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +7 -2
  157. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.d.ts +9 -0
  158. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.d.ts +19 -0
  159. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +6 -0
  160. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +10 -0
  161. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +34 -12
  162. package/dist/apps/shared/components/admin/settings/PluginsManager.d.ts +4 -0
  163. package/dist/apps/shared/components/admin/settings/PluginsManager.js +390 -78
  164. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.d.ts +9 -0
  165. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +40 -11
  166. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.d.ts +14 -0
  167. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +29 -13
  168. package/dist/apps/shared/components/admin/setup/SetupWizard.d.ts +36 -0
  169. package/dist/apps/shared/components/admin/setup/SetupWizard.js +87 -21
  170. package/dist/apps/shared/components/admin/signOut.d.ts +23 -0
  171. package/dist/apps/shared/components/admin/signOut.js +75 -0
  172. package/dist/apps/shared/components/admin/usePagedList.d.ts +39 -0
  173. package/dist/apps/shared/components/admin/usePagedList.js +99 -0
  174. package/dist/apps/shared/components/calendar/CalendarListSidebar.d.ts +26 -0
  175. package/dist/apps/shared/components/calendar/EventModal.d.ts +46 -0
  176. package/dist/apps/shared/components/calendar/EventModal.js +142 -21
  177. package/dist/apps/shared/components/calendar/MonthView.d.ts +15 -0
  178. package/dist/apps/shared/components/calendar/MonthView.js +12 -5
  179. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +20 -0
  180. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
  181. package/dist/apps/shared/components/calendar/ResourcePicker.d.ts +19 -0
  182. package/dist/apps/shared/components/calendar/SplitDayView.d.ts +28 -0
  183. package/dist/apps/shared/components/calendar/SplitDayView.js +10 -4
  184. package/dist/apps/shared/components/calendar/TimeGridView.d.ts +17 -0
  185. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
  186. package/dist/apps/shared/components/calendar/allDay.d.ts +51 -0
  187. package/dist/apps/shared/components/calendar/allDay.js +111 -0
  188. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +40 -0
  189. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +3 -3
  190. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +23 -0
  191. package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
  192. package/dist/apps/shared/components/contacts/ContactForm.d.ts +20 -0
  193. package/dist/apps/shared/components/contacts/ContactForm.js +32 -10
  194. package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +40 -0
  195. package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
  196. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +25 -0
  197. package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +40 -0
  198. package/dist/apps/shared/components/contacts/KeyChangeReview.js +63 -0
  199. package/dist/apps/shared/components/contacts/contactKeys.d.ts +29 -0
  200. package/dist/apps/shared/components/contacts/contactKeys.js +64 -0
  201. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +21 -0
  202. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +2 -2
  203. package/dist/apps/shared/components/escrow/layout/EscrowShell.d.ts +25 -0
  204. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +24 -5
  205. package/dist/apps/shared/components/forms/StringListField.d.ts +22 -0
  206. package/dist/apps/shared/components/layout/AppShell.d.ts +49 -0
  207. package/dist/apps/shared/components/layout/AppShell.js +97 -26
  208. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +18 -0
  209. package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
  210. package/dist/apps/shared/components/layout/KeyEnrollmentGate.d.ts +51 -0
  211. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +49 -12
  212. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +13 -0
  213. package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
  214. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +66 -0
  215. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
  216. package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +45 -0
  217. package/dist/apps/shared/components/layout/UnlockPromptProvider.js +73 -14
  218. package/dist/apps/shared/components/layout/UserMenu.d.ts +23 -0
  219. package/dist/apps/shared/components/mail/ConversationList.d.ts +11 -0
  220. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +23 -0
  221. package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
  222. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +93 -0
  223. package/dist/apps/shared/components/mail/MessageDetailPane.js +441 -36
  224. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +58 -0
  225. package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
  226. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +15 -0
  227. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +37 -0
  228. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +699 -128
  229. package/dist/apps/shared/components/mail/compose/EmojiPicker.d.ts +14 -0
  230. package/dist/apps/shared/components/mail/compose/GifPicker.d.ts +13 -0
  231. package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +34 -0
  232. package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.d.ts +16 -0
  233. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +27 -0
  234. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
  235. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +57 -0
  236. package/dist/apps/shared/components/mail/layout/MailShell.js +13 -4
  237. package/dist/apps/shared/components/mail/pinnedSigners.d.ts +33 -0
  238. package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
  239. package/dist/apps/shared/components/mail/verificationSeals.d.ts +22 -0
  240. package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
  241. package/dist/apps/shared/components/mail/writableMailboxes.d.ts +15 -0
  242. package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
  243. package/dist/apps/shared/components/rules/RuleBuilder.d.ts +70 -0
  244. package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
  245. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +41 -0
  246. package/dist/apps/shared/components/settings/layout/SettingsShell.js +19 -9
  247. package/dist/apps/shared/components/tasks/TasksSidebar.d.ts +41 -0
  248. package/dist/apps/shared/components/tasks/TasksToolbar.d.ts +14 -0
  249. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +24 -0
  250. package/dist/apps/shared/components/tasks/layout/TasksShell.js +2 -2
  251. package/dist/apps/shared/mail/findWellKnownFolderUid.d.ts +5 -0
  252. package/dist/apps/shared/mail/listAllPages.d.ts +17 -0
  253. package/dist/apps/shared/mail/listAllPages.js +25 -0
  254. package/dist/apps/shared/plugins/pluginNav.d.ts +39 -0
  255. package/dist/apps/shared/plugins/pluginNav.js +33 -0
  256. package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +13 -0
  257. package/dist/apps/shared/search/LocalIndexLifecycle.js +58 -54
  258. package/dist/apps/shared/search/localIndexBlockCipher.d.ts +36 -0
  259. package/dist/apps/shared/search/localIndexBuilder.d.ts +85 -0
  260. package/dist/apps/shared/search/localIndexBuilder.js +292 -37
  261. package/dist/apps/shared/search/localIndexKey.d.ts +4 -0
  262. package/dist/apps/shared/search/localIndexRpcClient.d.ts +71 -0
  263. package/dist/apps/shared/search/localIndexRpcClient.js +191 -26
  264. package/dist/apps/shared/search/localIndexSchema.d.ts +111 -0
  265. package/dist/apps/shared/search/localIndexSchema.js +12 -6
  266. package/dist/apps/shared/search/localIndexSizePreference.d.ts +27 -0
  267. package/dist/apps/shared/search/localIndexStorage.d.ts +28 -0
  268. package/dist/apps/shared/search/localIndexStorage.js +86 -0
  269. package/dist/apps/shared/search/localIndexVFS.d.ts +101 -0
  270. package/dist/apps/shared/search/localIndexVFS.js +68 -40
  271. package/dist/apps/shared/search/localIndexWorker.d.ts +145 -0
  272. package/dist/apps/shared/search/localIndexWorker.js +519 -173
  273. package/dist/apps/shared/search/searchTier2.d.ts +30 -0
  274. package/dist/apps/www/_404.d.ts +2 -0
  275. package/dist/apps/www/_500.d.ts +2 -0
  276. package/dist/apps/www/_layout.d.ts +14 -0
  277. package/dist/apps/www/calendar/index.d.ts +3 -0
  278. package/dist/apps/www/calendar/index.js +28 -10
  279. package/dist/apps/www/contacts/[uid].d.ts +13 -0
  280. package/dist/apps/www/contacts/[uid].js +13 -2
  281. package/dist/apps/www/contacts/index.d.ts +3 -0
  282. package/dist/apps/www/contacts/index.js +80 -10
  283. package/dist/apps/www/index.d.ts +3 -0
  284. package/dist/apps/www/index.js +339 -108
  285. package/dist/apps/www/messages/[uid].d.ts +11 -0
  286. package/dist/apps/www/messages/[uid].js +18 -6
  287. package/dist/apps/www/settings/auto-reply/index.d.ts +4 -0
  288. package/dist/apps/www/settings/auto-reply/index.js +9 -4
  289. package/dist/apps/www/settings/encryption/index.d.ts +7 -0
  290. package/dist/apps/www/settings/encryption/index.js +514 -97
  291. package/dist/apps/www/settings/filters/[uid].d.ts +8 -0
  292. package/dist/apps/www/settings/filters/[uid].js +22 -7
  293. package/dist/apps/www/settings/filters/_mailFilterRuleConfig.d.ts +11 -0
  294. package/dist/apps/www/settings/filters/index.d.ts +4 -0
  295. package/dist/apps/www/settings/filters/new/index.d.ts +4 -0
  296. package/dist/apps/www/settings/filters/new/index.js +8 -2
  297. package/dist/apps/www/settings/focused-inbox/index.d.ts +4 -0
  298. package/dist/apps/www/settings/focused-inbox/index.js +3 -1
  299. package/dist/apps/www/settings/labels/index.d.ts +4 -0
  300. package/dist/apps/www/settings/privacy/index.d.ts +4 -0
  301. package/dist/apps/www/settings/privacy/index.js +57 -22
  302. package/dist/apps/www/settings/read-receipts/index.d.ts +4 -0
  303. package/dist/apps/www/settings/read-receipts/index.js +10 -3
  304. package/dist/apps/www/settings/sharing/index.d.ts +4 -0
  305. package/dist/apps/www/settings/sharing/index.js +7 -11
  306. package/dist/apps/www/settings/signatures/[uid].d.ts +8 -0
  307. package/dist/apps/www/settings/signatures/index.d.ts +4 -0
  308. package/dist/apps/www/settings/signatures/new/index.d.ts +4 -0
  309. package/dist/apps/www/settings/signatures/signatureDefaults.d.ts +10 -0
  310. package/dist/apps/www/tasks/index.d.ts +3 -0
  311. package/dist/apps/www/tasks/index.js +50 -10
  312. package/package.json +3 -3
  313. package/apps/shared/components/booking/AvailabilityEditor.tsx +0 -118
  314. package/apps/www/settings/booking-types/[uid].tsx +0 -300
  315. package/apps/www/settings/booking-types/index.tsx +0 -98
  316. package/apps/www/settings/booking-types/new/index.tsx +0 -212
  317. package/dist/apps/shared/components/booking/AvailabilityEditor.js +0 -45
  318. package/dist/apps/www/settings/booking-types/[uid].js +0 -129
  319. package/dist/apps/www/settings/booking-types/index.js +0 -29
  320. package/dist/apps/www/settings/booking-types/new/index.js +0 -86
@@ -1,408 +1,420 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from "react";
6
- import { HiOutlineBars3 } from "react-icons/hi2";
7
- import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
- import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
9
- import { Folder, Mailbox, listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
10
- import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
- import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
12
- import AppShell, { AppShellProps } from "../../layout/AppShell.js";
13
- import KeyEnrollmentGate from "../../layout/KeyEnrollmentGate.js";
14
- import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
15
- import { useCompose } from "../compose/ComposeContext.js";
16
- import LocalIndexLifecycle from "../../../search/LocalIndexLifecycle.js";
17
-
18
- export type MailShellProps = Omit<AppShellProps, "active">;
19
-
20
- /** One mailbox's own mail folders (already filtered to `MAIL_FOLDER_TYPES` and unsorted) - one entry per
21
- * mailbox in `mailboxes`, fetched in parallel so every accessible mailbox's folder tree can render
22
- * simultaneously (see `MailShell`'s own doc comment on why this replaced the old single-mailbox `folders`
23
- * field). `error` is set (and `folders` left empty) for a mailbox whose own `listFolders()` call failed -
24
- * one mailbox's fetch failure must never blank out every other mailbox's section. */
25
- export interface MailboxFolders {
26
- mailbox: Mailbox;
27
- folders: Folder[];
28
- error?: string;
29
- }
30
-
31
- /** The well-known mail folder types an "aggregate" pseudo-folder can merge across every accessible
32
- * mailbox - a fixed, small set: `outbox` is deliberately excluded (transient per-mailbox send-queue
33
- * state, not a "merge across mailboxes" concept), and there's no aggregate concept for `calendar`/
34
- * `contacts`/`tasks`/`notes`/`user` folders (Mail's own sidebar already excludes those - see
35
- * `MAIL_FOLDER_TYPES`). */
36
- export const AGGREGATE_FOLDER_TYPES = ["inbox", "sent_items", "drafts", "deleted_items", "junk"] as const;
37
- export type AggregateFolderType = (typeof AGGREGATE_FOLDER_TYPES)[number];
38
-
39
- function isAggregateFolderType(value: string | null): value is AggregateFolderType {
40
- return !!value && (AGGREGATE_FOLDER_TYPES as readonly string[]).includes(value);
41
- }
42
-
43
- export interface MailShellContextValue {
44
- /** The mailbox currently selected (`?mailboxUid=`) - `undefined` while an aggregate pseudo-folder is
45
- * active instead (`aggregateFolderType` set), since there's no single mailbox to speak of then. */
46
- mailboxUid?: string;
47
- /** The folder currently selected (`?folderUid=`, or the selected mailbox's Inbox) - `undefined` in
48
- * aggregate mode, for the same reason. */
49
- folderUid?: string;
50
- /** Set only when an aggregate pseudo-folder (`?aggregate=inbox` etc.) is selected instead of a real,
51
- * single mailbox+folder - mutually exclusive with `mailboxUid`/`folderUid` above. */
52
- aggregateFolderType?: AggregateFolderType;
53
- mailboxes: Mailbox[];
54
- /** Every accessible mailbox's own mail folders - replaces the old single-mailbox `folders: Folder[]`
55
- * now that every mailbox's tree renders at once (see `MailboxFolders`'s own doc comment). */
56
- mailboxFolders: MailboxFolders[];
57
- }
58
-
59
- const MailShellContext = createContext<MailShellContextValue>({ mailboxes: [], mailboxFolders: [] });
60
-
61
- /** Reads the mailbox/folder a page is currently showing, as resolved by the enclosing `MailShell`. */
62
- export function useMailShell(): MailShellContextValue {
63
- return useContext(MailShellContext);
64
- }
65
-
66
- const FOLDER_LABELS: Record<string, string> = {
67
- inbox: "Inbox",
68
- sent_items: "Sent Items",
69
- drafts: "Drafts",
70
- outbox: "Outbox",
71
- junk: "Junk Email",
72
- archive: "Archive",
73
- deleted_items: "Deleted Items",
74
- };
75
-
76
- /** Well-known folders sort first, in Gmail/Outlook's conventional order; anything else (incl. `user`) sorts after, alphabetically. */
77
- const FOLDER_ORDER = ["inbox", "drafts", "outbox", "sent_items", "junk", "archive", "deleted_items"];
78
-
79
- /**
80
- * A mailbox's `calendar`/`contacts`/`tasks`/`notes` folders back their own dedicated apps (see
81
- * `CalendarShell`/`ContactsShell`/`TasksShell`), not Mail — `listFolders()` returns every well-known
82
- * folder for the mailbox regardless of which app owns it, so Mail's own folder tree must filter down
83
- * to just the mail ones itself, or those other apps' folders leak into this sidebar.
84
- */
85
- const MAIL_FOLDER_TYPES = new Set([...FOLDER_ORDER, "user"]);
86
-
87
- function folderSortKey(folder: Folder): number {
88
- const idx = FOLDER_ORDER.indexOf(folder.type);
89
- return idx === -1 ? FOLDER_ORDER.length : idx;
90
- }
91
-
92
- function sortedFoldersOf(folders: Folder[]): Folder[] {
93
- return [...folders].sort((a, b) => folderSortKey(a) - folderSortKey(b) || a.name.localeCompare(b.name));
94
- }
95
-
96
- function aggregateUnreadCount(mailboxFolders: MailboxFolders[], type: AggregateFolderType): number {
97
- return mailboxFolders.reduce((total, mf) => total + (mf.folders.find((f) => f.type === type)?.unreadCount ?? 0), 0);
98
- }
99
-
100
- type Status = "checking" | "error" | "ready";
101
-
102
- /**
103
- * A separate component (not inlined into `MailShell`'s own render) so `useCompose()` resolves against
104
- * `ComposeProvider` correctly: that provider is rendered *inside* the `AppShell` that `MailShell` itself
105
- * returns, i.e. a descendant of `MailShell`, not an ancestor — a hook call made directly in `MailShell`'s
106
- * own function body would see only whatever context exists *above* `MailShell`, never a provider one of
107
- * its own descendants creates. This button, rendered as part of `AppShell`'s `children`, sits correctly
108
- * inside that subtree.
109
- *
110
- * Opens with no mailbox: a fresh message defaults to the caller's own mailbox, and the compose window's
111
- * own From field is where the sender is chosen.
112
- */
113
- function ComposeButton() {
114
- const { openCompose } = useCompose();
115
- return (
116
- <button
117
- type="button"
118
- onClick={() => openCompose({})}
119
- className="block text-center w-full py-2.5 px-4 rounded-sm font-semibold text-sm bg-primary text-white hover:bg-primary-dark"
120
- >
121
- Compose
122
- </button>
123
- );
124
- }
125
-
126
- /**
127
- * Mail's own contextual sidebar (every accessible mailbox's own folder tree, plus a merged "All Mailboxes"
128
- * aggregate section) + content area, rendered inside the shared `AppShell` chrome (icon rail, header,
129
- * impersonation banner — see that component). There is no client-side router in this framework (see
130
- * `ReactRoute`'s file-convention resolver) — the selected mailbox/folder (or aggregate pseudo-folder) live
131
- * in the URL's `?mailboxUid=`/`?folderUid=`/`?aggregate=` query params, read once on mount (never during
132
- * the initial render itself, matching every other query-param reader in this codebase — e.g.
133
- * `apps/admin/quarantine`'s `readMailboxUid()` — so the server-rendered and just-hydrated client markup
134
- * match).
135
- *
136
- * Every accessible mailbox's folder tree renders at once - there is no "switch mailbox" affordance
137
- * anymore (the mailbox `<select>` this shell used to have is gone) - so a single-mailbox user sees zero
138
- * behavior change from before this, and a multi-mailbox user sees every mailbox's folders (and the new
139
- * aggregate section) simultaneously, matching the shared-mailbox feature's own "display as a separate set
140
- * of folders" requirement.
141
- */
142
- export default function MailShell({
143
- userUid,
144
- authServerUrl,
145
- impersonating,
146
- impersonationBaseUrl,
147
- trusted,
148
- children,
149
- }: PropsWithChildren<MailShellProps>) {
150
- const [status, setStatus] = useState<Status>("checking");
151
- const [error, setError] = useState<string | null>(null);
152
- const [mailboxes, setMailboxes] = useState<Mailbox[]>([]);
153
- const [mailboxFolders, setMailboxFolders] = useState<MailboxFolders[]>([]);
154
- const [foldersLoading, setFoldersLoading] = useState(true);
155
- const [requestedMailboxUid, setRequestedMailboxUid] = useState<string | null>(null);
156
- const [requestedFolderUid, setRequestedFolderUid] = useState<string | null>(null);
157
- const [requestedAggregateType, setRequestedAggregateType] = useState<string | null>(null);
158
- const [drawerOpen, setDrawerOpen] = useState(false);
159
-
160
- useEffect(() => {
161
- const params = new URLSearchParams(window.location.search);
162
- setRequestedMailboxUid(params.get("mailboxUid"));
163
- setRequestedFolderUid(params.get("folderUid"));
164
- setRequestedAggregateType(params.get("aggregate"));
165
- }, []);
166
-
167
- useEffect(() => {
168
- if (!userUid) {
169
- return;
170
- }
171
- listMailboxes({ limit: 100 })
172
- .then((result) => {
173
- setMailboxes(result);
174
- setStatus("ready");
175
- })
176
- .catch((err) => {
177
- setError(err instanceof ApiRequestError ? err.message : "Could not load your mailboxes.");
178
- setStatus("error");
179
- });
180
- }, [userUid]);
181
-
182
- const aggregateFolderType: AggregateFolderType | undefined = isAggregateFolderType(requestedAggregateType)
183
- ? requestedAggregateType
184
- : undefined;
185
-
186
- const mailboxUid: string | undefined = aggregateFolderType
187
- ? undefined
188
- : (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
189
- mailboxes[0]?.uid;
190
-
191
- // Fans out one listFolders() call per accessible mailbox in parallel - each call catches its own
192
- // failure into an MailboxFolders.error rather than letting Promise.all reject, so one mailbox's fetch
193
- // failure renders that section's own inline Alert instead of blanking out every other mailbox's
194
- // folder tree.
195
- useEffect(() => {
196
- if (mailboxes.length === 0) {
197
- // Deliberately leaves foldersLoading as-is: this also runs once before listMailboxes() has
198
- // resolved, and clearing it here would briefly render an empty sidebar the moment mailboxes
199
- // arrive, before their folders do. A genuinely mailbox-less caller gets MailboxProvisioning.
200
- setMailboxFolders([]);
201
- return;
202
- }
203
- setFoldersLoading(true);
204
- Promise.all(
205
- mailboxes.map((mailbox) =>
206
- listFolders(mailbox.uid)
207
- .then((result): MailboxFolders => ({ mailbox, folders: result.filter((f) => MAIL_FOLDER_TYPES.has(f.type)) }))
208
- .catch(
209
- (err): MailboxFolders => ({
210
- mailbox,
211
- folders: [],
212
- error: err instanceof ApiRequestError ? err.message : "Could not load folders.",
213
- }),
214
- ),
215
- ),
216
- )
217
- .then(setMailboxFolders)
218
- .finally(() => setFoldersLoading(false));
219
- }, [mailboxes]);
220
-
221
- const selectedMailboxFolders = mailboxFolders.find((mf) => mf.mailbox.uid === mailboxUid)?.folders ?? [];
222
- const folderUid: string | undefined = aggregateFolderType
223
- ? undefined
224
- : (requestedFolderUid && selectedMailboxFolders.some((f) => f.uid === requestedFolderUid) ? requestedFolderUid : undefined) ??
225
- selectedMailboxFolders.find((f) => f.type === "inbox")?.uid;
226
-
227
- // The mailbox `KeyEnrollmentGate`/`LocalIndexLifecycle`/`ComposeButton` treat as "the" mailbox when
228
- // there's no single selected one to use (aggregate mode) - the caller's own owned mailbox if they
229
- // have one, else whichever accessible mailbox happens to be first. See MailShell's own doc comment on
230
- // this being an accepted limitation: an aggregate-view message from a *different*, not-yet-visited
231
- // mailbox may still need that mailbox's own folder view opened directly to unlock/decrypt it.
232
- const defaultMailboxUid = mailboxes.find((mb) => mb.ownerUserUid === userUid)?.uid ?? mailboxes[0]?.uid;
233
- const activeMailboxUid = mailboxUid ?? defaultMailboxUid;
234
-
235
- const contextValue = useMemo<MailShellContextValue>(
236
- () => ({ mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders }),
237
- [mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders],
238
- );
239
-
240
- // A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else for a
241
- // mailbox-less caller to do here yet, so the icon rail/header/folder tree don't render at all. Checks
242
- // `mailboxes.length` directly (not `!mailboxUid`) since `mailboxUid` is legitimately undefined in
243
- // aggregate mode even with mailboxes present.
244
- if (userUid && status === "ready" && mailboxes.length === 0) {
245
- return <MailboxProvisioning />;
246
- }
247
-
248
- let inner: ReactNode = null;
249
- if (userUid && status === "checking") {
250
- // Renders immediately (no network round trip needed) so switching into Mail never shows a blank
251
- // pane while `listMailboxes()` is in flight — the outline below mirrors the real sidebar's shape.
252
- inner = (
253
- <aside className="w-64 shrink-0 bg-surface border-r border-border flex flex-col p-3 gap-4">
254
- <Skeleton height="h-9" className="rounded-sm" />
255
- <SkeletonList count={6} />
256
- </aside>
257
- );
258
- } else if (userUid && status === "error") {
259
- inner = (
260
- <div className="flex-1 flex items-center justify-center p-8">
261
- <div className="w-full max-w-md">
262
- <Alert>{error}</Alert>
263
- </div>
264
- </div>
265
- );
266
- } else if (userUid && status === "ready") {
267
- // A function, not a plain JSX constant — it's rendered twice (desktop `<aside>` + mobile
268
- // `Drawer`), possibly *simultaneously* mounted (the aside is only CSS-hidden below `md`, not
269
- // unmounted).
270
- const sidebarContent = () => (
271
- <>
272
- <div className="p-3">
273
- <ComposeButton />
274
- </div>
275
- {foldersLoading ? (
276
- <div className="flex-1 overflow-y-auto px-3 pb-3">
277
- <SkeletonList count={6} className="pt-1" />
278
- </div>
279
- ) : (
280
- <nav className="flex-1 overflow-y-auto px-3 pb-3 flex flex-col gap-3">
281
- {mailboxes.length > 1 && (
282
- <div>
283
- <div className="text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5">
284
- All Mailboxes
285
- </div>
286
- <div className="flex flex-col gap-0.5">
287
- {AGGREGATE_FOLDER_TYPES.map((type) => {
288
- const unread = aggregateUnreadCount(mailboxFolders, type);
289
- return (
290
- <a
291
- key={type}
292
- href={`/?aggregate=${encodeURIComponent(type)}`}
293
- className={[
294
- "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
295
- aggregateFolderType === type
296
- ? "bg-primary/10 text-primary-dark font-semibold"
297
- : "text-text hover:bg-surface-alt",
298
- ].join(" ")}
299
- >
300
- <span>{FOLDER_LABELS[type]}</span>
301
- {unread > 0 && (
302
- <span className="text-xs font-bold rounded-pill py-0.5 px-1.5 bg-surface-alt text-text-muted">
303
- {unread}
304
- </span>
305
- )}
306
- </a>
307
- );
308
- })}
309
- </div>
310
- </div>
311
- )}
312
- {mailboxFolders.map(({ mailbox, folders, error: mailboxError }) => (
313
- <div key={mailbox.uid}>
314
- <div className="text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5 truncate">
315
- {mailbox.displayName}
316
- {mailbox.ownerUserUid ? "" : " (shared)"}
317
- </div>
318
- {mailboxError && (
319
- <div className="px-2.5 pb-1">
320
- <Alert>{mailboxError}</Alert>
321
- </div>
322
- )}
323
- <div className="flex flex-col gap-0.5">
324
- {sortedFoldersOf(folders).map((folder) => (
325
- <a
326
- key={folder.uid}
327
- href={`/?mailboxUid=${encodeURIComponent(mailbox.uid)}&folderUid=${encodeURIComponent(folder.uid)}`}
328
- className={[
329
- "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
330
- folder.uid === folderUid
331
- ? "bg-primary/10 text-primary-dark font-semibold"
332
- : "text-text hover:bg-surface-alt",
333
- ].join(" ")}
334
- >
335
- <span>{FOLDER_LABELS[folder.type] ?? folder.name}</span>
336
- {folder.unreadCount > 0 && (
337
- <span className="text-xs font-bold rounded-pill py-0.5 px-1.5 bg-surface-alt text-text-muted">
338
- {folder.unreadCount}
339
- </span>
340
- )}
341
- </a>
342
- ))}
343
- </div>
344
- </div>
345
- ))}
346
- </nav>
347
- )}
348
- </>
349
- );
350
-
351
- inner = (
352
- <>
353
- <aside className="hidden md:flex w-64 shrink-0 bg-surface border-r border-border flex-col">{sidebarContent()}</aside>
354
- <Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Folders">
355
- <div className="flex flex-col">{sidebarContent()}</div>
356
- </Drawer>
357
- <main className="flex-1 min-w-0 overflow-y-auto">
358
- <button
359
- type="button"
360
- className="md:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text"
361
- aria-label="Open folders"
362
- onClick={() => setDrawerOpen(true)}
363
- >
364
- <HiOutlineBars3 size={20} aria-hidden="true" />
365
- </button>
366
- <MailShellContext.Provider value={contextValue}>{children}</MailShellContext.Provider>
367
- </main>
368
- </>
369
- );
370
- }
371
-
372
- // Wraps AppShell unconditionally, at a stable tree position regardless of whether mailboxUid has
373
- // resolved yet - KeyEnrollmentGate itself passes `children` through untouched until a real
374
- // mailboxUid is supplied (see its own doc comment). Wrapping only once ready (a conditional tree
375
- // position) would make AppShell itself remount the moment mailboxUid resolves, tearing down
376
- // whatever state/effects it had already started (confirmed by direct reproduction: the
377
- // impersonation banner's own internal state was lost exactly at that transition).
378
- const activeMailbox = mailboxes.find((mb) => mb.uid === activeMailboxUid);
379
- return (
380
- <KeyEnrollmentGate
381
- mailboxUid={activeMailboxUid}
382
- mailboxAddress={activeMailbox?.primarySmtpAddress}
383
- mailboxKeys={activeMailbox?.keys}
384
- // Unlocking is only actually required to sign/encrypt a compose, read an already-encrypted
385
- // message, or change encryption settings - not merely to open Mail. Those specific call sites
386
- // (ComposeWindow, MessageDetailPane) request an unlock on demand via useUnlockPrompt() instead.
387
- // First-time provisioning (a mailbox with no vault at all yet) still always blocks - see this
388
- // prop's own doc comment on KeyEnrollmentGateProps.
389
- blocking={false}
390
- >
391
- <LocalIndexLifecycle
392
- mailboxUid={activeMailboxUid}
393
- mailboxKeys={activeMailbox?.keys}
394
- folders={mailboxFolders.find((mf) => mf.mailbox.uid === activeMailboxUid)?.folders ?? []}
395
- />
396
- <AppShell
397
- active="mail"
398
- userUid={userUid}
399
- authServerUrl={authServerUrl}
400
- impersonating={impersonating}
401
- impersonationBaseUrl={impersonationBaseUrl}
402
- trusted={trusted}
403
- >
404
- {inner}
405
- </AppShell>
406
- </KeyEnrollmentGate>
407
- );
408
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from "react";
6
+ import { HiOutlineBars3 } from "react-icons/hi2";
7
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
+ import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
9
+ import { Folder, Mailbox, listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
10
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
+ import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
12
+ import AppShell, { AppShellProps } from "../../layout/AppShell.js";
13
+ import KeyEnrollmentGate from "../../layout/KeyEnrollmentGate.js";
14
+ import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
15
+ import { useCompose } from "../compose/ComposeContext.js";
16
+ import LocalIndexLifecycle from "../../../search/LocalIndexLifecycle.js";
17
+
18
+ export type MailShellProps = Omit<AppShellProps, "active">;
19
+
20
+ /** One mailbox's own mail folders (already filtered to `MAIL_FOLDER_TYPES` and unsorted) - one entry per
21
+ * mailbox in `mailboxes`, fetched in parallel so every accessible mailbox's folder tree can render
22
+ * simultaneously (see `MailShell`'s own doc comment on why this replaced the old single-mailbox `folders`
23
+ * field). `error` is set (and `folders` left empty) for a mailbox whose own `listFolders()` call failed -
24
+ * one mailbox's fetch failure must never blank out every other mailbox's section. */
25
+ export interface MailboxFolders {
26
+ mailbox: Mailbox;
27
+ folders: Folder[];
28
+ error?: string;
29
+ }
30
+
31
+ /** The well-known mail folder types an "aggregate" pseudo-folder can merge across every accessible
32
+ * mailbox - a fixed, small set: `outbox` is deliberately excluded (transient per-mailbox send-queue
33
+ * state, not a "merge across mailboxes" concept), and there's no aggregate concept for `calendar`/
34
+ * `contacts`/`tasks`/`notes`/`user` folders (Mail's own sidebar already excludes those - see
35
+ * `MAIL_FOLDER_TYPES`). */
36
+ export const AGGREGATE_FOLDER_TYPES = ["inbox", "sent_items", "drafts", "deleted_items", "junk"] as const;
37
+ export type AggregateFolderType = (typeof AGGREGATE_FOLDER_TYPES)[number];
38
+
39
+ function isAggregateFolderType(value: string | null): value is AggregateFolderType {
40
+ return !!value && (AGGREGATE_FOLDER_TYPES as readonly string[]).includes(value);
41
+ }
42
+
43
+ export interface MailShellContextValue {
44
+ /** The mailbox currently selected (`?mailboxUid=`) - `undefined` while an aggregate pseudo-folder is
45
+ * active instead (`aggregateFolderType` set), since there's no single mailbox to speak of then. */
46
+ mailboxUid?: string;
47
+ /** The folder currently selected (`?folderUid=`, or the selected mailbox's Inbox) - `undefined` in
48
+ * aggregate mode, for the same reason. */
49
+ folderUid?: string;
50
+ /** Set only when an aggregate pseudo-folder (`?aggregate=inbox` etc.) is selected instead of a real,
51
+ * single mailbox+folder - mutually exclusive with `mailboxUid`/`folderUid` above. */
52
+ aggregateFolderType?: AggregateFolderType;
53
+ mailboxes: Mailbox[];
54
+ /** Every accessible mailbox's own mail folders - replaces the old single-mailbox `folders: Folder[]`
55
+ * now that every mailbox's tree renders at once (see `MailboxFolders`'s own doc comment). */
56
+ mailboxFolders: MailboxFolders[];
57
+ }
58
+
59
+ const MailShellContext = createContext<MailShellContextValue>({ mailboxes: [], mailboxFolders: [] });
60
+
61
+ /** Reads the mailbox/folder a page is currently showing, as resolved by the enclosing `MailShell`. */
62
+ export function useMailShell(): MailShellContextValue {
63
+ return useContext(MailShellContext);
64
+ }
65
+
66
+ const FOLDER_LABELS: Record<string, string> = {
67
+ inbox: "Inbox",
68
+ sent_items: "Sent Items",
69
+ drafts: "Drafts",
70
+ outbox: "Outbox",
71
+ junk: "Junk Email",
72
+ archive: "Archive",
73
+ deleted_items: "Deleted Items",
74
+ };
75
+
76
+ /** Well-known folders sort first, in Gmail/Outlook's conventional order; anything else (incl. `user`) sorts after, alphabetically. */
77
+ const FOLDER_ORDER = ["inbox", "drafts", "outbox", "sent_items", "junk", "archive", "deleted_items"];
78
+
79
+ /**
80
+ * A mailbox's `calendar`/`contacts`/`tasks`/`notes` folders back their own dedicated apps (see
81
+ * `CalendarShell`/`ContactsShell`/`TasksShell`), not Mail — `listFolders()` returns every well-known
82
+ * folder for the mailbox regardless of which app owns it, so Mail's own folder tree must filter down
83
+ * to just the mail ones itself, or those other apps' folders leak into this sidebar.
84
+ */
85
+ const MAIL_FOLDER_TYPES = new Set([...FOLDER_ORDER, "user"]);
86
+
87
+ function folderSortKey(folder: Folder): number {
88
+ const idx = FOLDER_ORDER.indexOf(folder.type);
89
+ return idx === -1 ? FOLDER_ORDER.length : idx;
90
+ }
91
+
92
+ function sortedFoldersOf(folders: Folder[]): Folder[] {
93
+ return [...folders].sort((a, b) => folderSortKey(a) - folderSortKey(b) || a.name.localeCompare(b.name));
94
+ }
95
+
96
+ function aggregateUnreadCount(mailboxFolders: MailboxFolders[], type: AggregateFolderType): number {
97
+ return mailboxFolders.reduce((total, mf) => total + (mf.folders.find((f) => f.type === type)?.unreadCount ?? 0), 0);
98
+ }
99
+
100
+ type Status = "checking" | "error" | "ready";
101
+
102
+ /** Page size of the one `listMailboxes()` call this shell makes. */
103
+ export const MAILBOX_LIST_LIMIT = 100;
104
+
105
+ /**
106
+ * A separate component (not inlined into `MailShell`'s own render) so `useCompose()` resolves against
107
+ * `ComposeProvider` correctly: that provider is rendered *inside* the `AppShell` that `MailShell` itself
108
+ * returns, i.e. a descendant of `MailShell`, not an ancestor — a hook call made directly in `MailShell`'s
109
+ * own function body would see only whatever context exists *above* `MailShell`, never a provider one of
110
+ * its own descendants creates. This button, rendered as part of `AppShell`'s `children`, sits correctly
111
+ * inside that subtree.
112
+ *
113
+ * Opens with no mailbox: a fresh message defaults to the caller's own mailbox, and the compose window's
114
+ * own From field is where the sender is chosen.
115
+ */
116
+ function ComposeButton() {
117
+ const { openCompose } = useCompose();
118
+ return (
119
+ <button
120
+ type="button"
121
+ onClick={() => openCompose({})}
122
+ className="block text-center w-full py-2.5 px-4 rounded-sm font-semibold text-sm bg-primary text-white hover:bg-primary-dark"
123
+ >
124
+ Compose
125
+ </button>
126
+ );
127
+ }
128
+
129
+ /**
130
+ * Mail's own contextual sidebar (every accessible mailbox's own folder tree, plus a merged "All Mailboxes"
131
+ * aggregate section) + content area, rendered inside the shared `AppShell` chrome (icon rail, header,
132
+ * impersonation banner — see that component). There is no client-side router in this framework (see
133
+ * `ReactRoute`'s file-convention resolver) — the selected mailbox/folder (or aggregate pseudo-folder) live
134
+ * in the URL's `?mailboxUid=`/`?folderUid=`/`?aggregate=` query params, read once on mount (never during
135
+ * the initial render itself, matching every other query-param reader in this codebase — e.g.
136
+ * `apps/admin/quarantine`'s `readMailboxUid()` — so the server-rendered and just-hydrated client markup
137
+ * match).
138
+ *
139
+ * Every accessible mailbox's folder tree renders at once - there is no "switch mailbox" affordance
140
+ * anymore (the mailbox `<select>` this shell used to have is gone) - so a single-mailbox user sees zero
141
+ * behavior change from before this, and a multi-mailbox user sees every mailbox's folders (and the new
142
+ * aggregate section) simultaneously, matching the shared-mailbox feature's own "display as a separate set
143
+ * of folders" requirement.
144
+ */
145
+ export default function MailShell({
146
+ userUid,
147
+ authServerUrl,
148
+ impersonating,
149
+ impersonationBaseUrl,
150
+ trusted,
151
+ pluginNav,
152
+ children,
153
+ }: PropsWithChildren<MailShellProps>) {
154
+ const [status, setStatus] = useState<Status>("checking");
155
+ const [error, setError] = useState<string | null>(null);
156
+ const [mailboxes, setMailboxes] = useState<Mailbox[]>([]);
157
+ const [mailboxFolders, setMailboxFolders] = useState<MailboxFolders[]>([]);
158
+ const [foldersLoading, setFoldersLoading] = useState(true);
159
+ const [requestedMailboxUid, setRequestedMailboxUid] = useState<string | null>(null);
160
+ const [requestedFolderUid, setRequestedFolderUid] = useState<string | null>(null);
161
+ const [requestedAggregateType, setRequestedAggregateType] = useState<string | null>(null);
162
+ const [drawerOpen, setDrawerOpen] = useState(false);
163
+
164
+ useEffect(() => {
165
+ const params = new URLSearchParams(window.location.search);
166
+ setRequestedMailboxUid(params.get("mailboxUid"));
167
+ setRequestedFolderUid(params.get("folderUid"));
168
+ setRequestedAggregateType(params.get("aggregate"));
169
+ }, []);
170
+
171
+ useEffect(() => {
172
+ if (!userUid) {
173
+ return;
174
+ }
175
+ listMailboxes({ limit: MAILBOX_LIST_LIMIT })
176
+ .then((result) => {
177
+ setMailboxes(result);
178
+ setStatus("ready");
179
+ })
180
+ .catch((err) => {
181
+ setError(err instanceof ApiRequestError ? err.message : "Could not load your mailboxes.");
182
+ setStatus("error");
183
+ });
184
+ }, [userUid]);
185
+
186
+ const aggregateFolderType: AggregateFolderType | undefined = isAggregateFolderType(requestedAggregateType)
187
+ ? requestedAggregateType
188
+ : undefined;
189
+
190
+ const mailboxUid: string | undefined = aggregateFolderType
191
+ ? undefined
192
+ : (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
193
+ mailboxes[0]?.uid;
194
+
195
+ // Fans out one listFolders() call per accessible mailbox in parallel - each call catches its own
196
+ // failure into an MailboxFolders.error rather than letting Promise.all reject, so one mailbox's fetch
197
+ // failure renders that section's own inline Alert instead of blanking out every other mailbox's
198
+ // folder tree.
199
+ useEffect(() => {
200
+ if (mailboxes.length === 0) {
201
+ // Deliberately leaves foldersLoading as-is: this also runs once before listMailboxes() has
202
+ // resolved, and clearing it here would briefly render an empty sidebar the moment mailboxes
203
+ // arrive, before their folders do. A genuinely mailbox-less caller gets MailboxProvisioning.
204
+ setMailboxFolders([]);
205
+ return;
206
+ }
207
+ setFoldersLoading(true);
208
+ void Promise.all(
209
+ mailboxes.map((mailbox) =>
210
+ listFolders(mailbox.uid)
211
+ .then((result): MailboxFolders => ({ mailbox, folders: result.filter((f) => MAIL_FOLDER_TYPES.has(f.type)) }))
212
+ .catch(
213
+ (err): MailboxFolders => ({
214
+ mailbox,
215
+ folders: [],
216
+ error: err instanceof ApiRequestError ? err.message : "Could not load folders.",
217
+ }),
218
+ ),
219
+ ),
220
+ )
221
+ .then(setMailboxFolders)
222
+ .finally(() => setFoldersLoading(false));
223
+ }, [mailboxes]);
224
+
225
+ const selectedMailboxFolders = mailboxFolders.find((mf) => mf.mailbox.uid === mailboxUid)?.folders ?? [];
226
+ const folderUid: string | undefined = aggregateFolderType
227
+ ? undefined
228
+ : (requestedFolderUid && selectedMailboxFolders.some((f) => f.uid === requestedFolderUid) ? requestedFolderUid : undefined) ??
229
+ selectedMailboxFolders.find((f) => f.type === "inbox")?.uid;
230
+
231
+ // The mailbox `KeyEnrollmentGate`/`LocalIndexLifecycle`/`ComposeButton` treat as "the" mailbox when
232
+ // there's no single selected one to use (aggregate mode) - the caller's own owned mailbox if they
233
+ // have one, else whichever accessible mailbox happens to be first. See MailShell's own doc comment on
234
+ // this being an accepted limitation: an aggregate-view message from a *different*, not-yet-visited
235
+ // mailbox may still need that mailbox's own folder view opened directly to unlock/decrypt it.
236
+ const defaultMailboxUid = mailboxes.find((mb) => mb.ownerUserUid === userUid)?.uid ?? mailboxes[0]?.uid;
237
+ const activeMailboxUid = mailboxUid ?? defaultMailboxUid;
238
+
239
+ const contextValue = useMemo<MailShellContextValue>(
240
+ () => ({ mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders }),
241
+ [mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders],
242
+ );
243
+
244
+ // A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else for a
245
+ // mailbox-less caller to do here yet, so the icon rail/header/folder tree don't render at all. Checks
246
+ // `mailboxes.length` directly (not `!mailboxUid`) since `mailboxUid` is legitimately undefined in
247
+ // aggregate mode even with mailboxes present.
248
+ if (userUid && status === "ready" && mailboxes.length === 0) {
249
+ return <MailboxProvisioning />;
250
+ }
251
+
252
+ let inner: ReactNode = null;
253
+ if (userUid && status === "checking") {
254
+ // Renders immediately (no network round trip needed) so switching into Mail never shows a blank
255
+ // pane while `listMailboxes()` is in flight — the outline below mirrors the real sidebar's shape.
256
+ inner = (
257
+ <aside className="w-64 shrink-0 bg-surface border-r border-border flex flex-col p-3 gap-4">
258
+ <Skeleton height="h-9" className="rounded-sm" />
259
+ <SkeletonList count={6} />
260
+ </aside>
261
+ );
262
+ } else if (userUid && status === "error") {
263
+ inner = (
264
+ <div className="flex-1 flex items-center justify-center p-8">
265
+ <div className="w-full max-w-md">
266
+ <Alert>{error}</Alert>
267
+ </div>
268
+ </div>
269
+ );
270
+ } else if (userUid && status === "ready") {
271
+ // A function, not a plain JSX constant — it's rendered twice (desktop `<aside>` + mobile
272
+ // `Drawer`), possibly *simultaneously* mounted (the aside is only CSS-hidden below `md`, not
273
+ // unmounted).
274
+ const sidebarContent = () => (
275
+ <>
276
+ <div className="p-3">
277
+ <ComposeButton />
278
+ </div>
279
+ {foldersLoading ? (
280
+ <div className="flex-1 overflow-y-auto px-3 pb-3">
281
+ <SkeletonList count={6} className="pt-1" />
282
+ </div>
283
+ ) : (
284
+ <nav className="flex-1 overflow-y-auto px-3 pb-3 flex flex-col gap-3">
285
+ {mailboxes.length > 1 && (
286
+ <div>
287
+ <div className="text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5">
288
+ All Mailboxes
289
+ </div>
290
+ <div className="flex flex-col gap-0.5">
291
+ {AGGREGATE_FOLDER_TYPES.map((type) => {
292
+ const unread = aggregateUnreadCount(mailboxFolders, type);
293
+ return (
294
+ <a
295
+ key={type}
296
+ href={`/?aggregate=${encodeURIComponent(type)}`}
297
+ className={[
298
+ "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
299
+ aggregateFolderType === type
300
+ ? "bg-primary/10 text-primary-dark font-semibold"
301
+ : "text-text hover:bg-surface-alt",
302
+ ].join(" ")}
303
+ >
304
+ <span>{FOLDER_LABELS[type]}</span>
305
+ {unread > 0 && (
306
+ <span className="text-xs font-bold rounded-pill py-0.5 px-1.5 bg-surface-alt text-text-muted">
307
+ {unread}
308
+ </span>
309
+ )}
310
+ </a>
311
+ );
312
+ })}
313
+ </div>
314
+ </div>
315
+ )}
316
+ {mailboxFolders.map(({ mailbox, folders, error: mailboxError }) => (
317
+ <div key={mailbox.uid}>
318
+ <div className="text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5 truncate">
319
+ {mailbox.displayName}
320
+ {mailbox.ownerUserUid ? "" : " (shared)"}
321
+ </div>
322
+ {mailboxError && (
323
+ <div className="px-2.5 pb-1">
324
+ <Alert>{mailboxError}</Alert>
325
+ </div>
326
+ )}
327
+ <div className="flex flex-col gap-0.5">
328
+ {sortedFoldersOf(folders).map((folder) => (
329
+ <a
330
+ key={folder.uid}
331
+ href={`/?mailboxUid=${encodeURIComponent(mailbox.uid)}&folderUid=${encodeURIComponent(folder.uid)}`}
332
+ className={[
333
+ "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
334
+ folder.uid === folderUid
335
+ ? "bg-primary/10 text-primary-dark font-semibold"
336
+ : "text-text hover:bg-surface-alt",
337
+ ].join(" ")}
338
+ >
339
+ <span>{FOLDER_LABELS[folder.type] ?? folder.name}</span>
340
+ {folder.unreadCount > 0 && (
341
+ <span className="text-xs font-bold rounded-pill py-0.5 px-1.5 bg-surface-alt text-text-muted">
342
+ {folder.unreadCount}
343
+ </span>
344
+ )}
345
+ </a>
346
+ ))}
347
+ </div>
348
+ </div>
349
+ ))}
350
+ </nav>
351
+ )}
352
+ </>
353
+ );
354
+
355
+ inner = (
356
+ <>
357
+ <aside className="hidden md:flex w-64 shrink-0 bg-surface border-r border-border flex-col">{sidebarContent()}</aside>
358
+ <Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Folders">
359
+ <div className="flex flex-col">{sidebarContent()}</div>
360
+ </Drawer>
361
+ <main className="flex-1 min-w-0 overflow-y-auto">
362
+ <button
363
+ type="button"
364
+ className="md:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text"
365
+ aria-label="Open folders"
366
+ onClick={() => setDrawerOpen(true)}
367
+ >
368
+ <HiOutlineBars3 size={20} aria-hidden="true" />
369
+ </button>
370
+ <MailShellContext.Provider value={contextValue}>{children}</MailShellContext.Provider>
371
+ </main>
372
+ </>
373
+ );
374
+ }
375
+
376
+ // Wraps AppShell unconditionally, at a stable tree position regardless of whether mailboxUid has
377
+ // resolved yet - KeyEnrollmentGate itself passes `children` through untouched until a real
378
+ // mailboxUid is supplied (see its own doc comment). Wrapping only once ready (a conditional tree
379
+ // position) would make AppShell itself remount the moment mailboxUid resolves, tearing down
380
+ // whatever state/effects it had already started (confirmed by direct reproduction: the
381
+ // impersonation banner's own internal state was lost exactly at that transition).
382
+ const activeMailbox = mailboxes.find((mb) => mb.uid === activeMailboxUid);
383
+ return (
384
+ <KeyEnrollmentGate
385
+ mailboxUid={activeMailboxUid}
386
+ mailboxAddress={activeMailbox?.primarySmtpAddress}
387
+ mailboxKeys={activeMailbox?.keys}
388
+ // Unlocking is only actually required to sign/encrypt a compose, read an already-encrypted
389
+ // message, or change encryption settings - not merely to open Mail. Those specific call sites
390
+ // (ComposeWindow, MessageDetailPane) request an unlock on demand via useUnlockPrompt() instead.
391
+ // First-time provisioning (a mailbox with no vault at all yet) still always blocks - see this
392
+ // prop's own doc comment on KeyEnrollmentGateProps.
393
+ blocking={false}
394
+ // A shared/delegated mailbox the caller doesn't own is never provisioned from here - its keys
395
+ // belong to its owner (or its admins), not to whoever happens to open it first - and neither is
396
+ // the impersonated user's own mailbox: an administrator must never choose its password/recovery codes.
397
+ canProvision={!impersonating && activeMailbox?.ownerUserUid === userUid}
398
+ >
399
+ <LocalIndexLifecycle
400
+ mailboxUid={activeMailboxUid}
401
+ mailboxKeys={activeMailbox?.keys}
402
+ folders={mailboxFolders.find((mf) => mf.mailbox.uid === activeMailboxUid)?.folders ?? []}
403
+ // A full page may be truncated - pruning against it would delete indexes of accessible mailboxes
404
+ // beyond the limit, so it's only trusted as the complete list when it came back short.
405
+ accessibleMailboxUids={status === "ready" && mailboxes.length < MAILBOX_LIST_LIMIT ? mailboxes.map((mb) => mb.uid) : undefined}
406
+ />
407
+ <AppShell
408
+ active="mail"
409
+ userUid={userUid}
410
+ authServerUrl={authServerUrl}
411
+ impersonating={impersonating}
412
+ impersonationBaseUrl={impersonationBaseUrl}
413
+ trusted={trusted}
414
+ pluginNav={pluginNav}
415
+ >
416
+ {inner}
417
+ </AppShell>
418
+ </KeyEnrollmentGate>
419
+ );
420
+ }