@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
@@ -0,0 +1,14 @@
1
+ import React, { RefObject } from "react";
2
+ export interface EmojiPickerProps {
3
+ anchorRef: RefObject<HTMLElement | null>;
4
+ onSelect: (native: string) => void;
5
+ onClose: () => void;
6
+ }
7
+ /**
8
+ * "Insert emoji" popup — a scrollable grid of every standard unicode emoji, grouped by category (see
9
+ * `emojiData.ts`). Deliberately no search box, unlike `GifPicker`'s — the user's own request only
10
+ * asked for a browsable grid here, and 8 category headers over ~1900 emojis is already a reasonably
11
+ * navigable amount without one. Rendered via `PopoverPortal` — see that component's own doc comment
12
+ * on why (escaping the toolbar's `overflow-hidden` ancestors).
13
+ */
14
+ export default function EmojiPicker({ anchorRef, onSelect, onClose }: EmojiPickerProps): React.JSX.Element;
@@ -0,0 +1,13 @@
1
+ import React, { RefObject } from "react";
2
+ export interface GifPickerProps {
3
+ anchorRef: RefObject<HTMLElement | null>;
4
+ onSelect: (url: string) => void;
5
+ onClose: () => void;
6
+ }
7
+ /** "Insert GIF" popup — a search box plus a scrollable grid of Giphy thumbnails (trending by default,
8
+ * live-searched as the user types). Selecting one inserts its full-resolution URL into the message
9
+ * body — see `ComposeToolbar.tsx`'s own doc comment on why that's a plain `<img>` reference rather
10
+ * than an uploaded attachment (unlike "Insert image"): a GIF is already hosted at a stable public
11
+ * URL, so there's nothing to upload. Rendered via `PopoverPortal` — see that component's own doc
12
+ * comment on why (escaping the toolbar's `overflow-hidden` ancestors). */
13
+ export default function GifPicker({ anchorRef, onSelect, onClose }: GifPickerProps): React.JSX.Element;
@@ -0,0 +1,34 @@
1
+ import React from "react";
2
+ export interface RichTextEditorProps {
3
+ value: string;
4
+ onChange: (value: string) => void;
5
+ height?: string;
6
+ /** When true, the editor fills its parent's height via flexbox instead of a fixed `height` — for
7
+ * embedding inside a flex-column container of variable height (the floating Compose window) where
8
+ * a literal pixel height can't be computed up front. `height` is ignored when this is set. */
9
+ fill?: boolean;
10
+ /** Passed straight through to `ComposeToolbar` — see its own doc comment on this prop. */
11
+ onUploadImage: (file: File) => Promise<string | null>;
12
+ }
13
+ /**
14
+ * A WYSIWYG rich-text editor for the Compose window's message body — replaces `MonacoHtmlEditor` (an HTML
15
+ * *source* editor) with a real Outlook-Home-tab-style formatting experience, built on TipTap/ProseMirror.
16
+ * Used by `ComposeWindow` (the floating overlay, not a dedicated page — see `ComposeContext.tsx`'s doc
17
+ * comment).
18
+ *
19
+ * `value` seeds the editor's initial content only — like `MonacoHtmlEditor`, this never re-syncs from a
20
+ * later `value` prop change; TipTap owns its own document once created. Live edits flow out as HTML (via
21
+ * `editor.getHTML()`) through `onChange`.
22
+ *
23
+ * `immediatelyRender: false` is required for this framework's SSR: the page embedding `ComposeWindow` is
24
+ * rendered server-side under plain Node before hydration (see `ReactRoute`), and TipTap's default
25
+ * (`immediatelyRender: true`) tries to mount its ProseMirror view during that very first render, which
26
+ * both can't work without a real DOM and produces a hydration mismatch. With it `false`, `useEditor`
27
+ * returns `null` until the editor actually mounts client-side after hydration — `ComposeToolbar`/
28
+ * `EditorContent` both already handle a `null` editor by rendering their disabled/empty state, so this
29
+ * needs no extra loading-placeholder logic here. Unlike `MonacoHtmlEditor`, no dynamic `import()` is
30
+ * needed to defer loading TipTap's modules off the SSR path — `immediatelyRender: false` alone is
31
+ * sufficient here since TipTap (unlike Monaco) never touches the DOM at module-evaluation time, only when
32
+ * an editor view actually mounts.
33
+ */
34
+ export default function RichTextEditor({ value, onChange, height, fill, onUploadImage }: RichTextEditorProps): React.JSX.Element;
@@ -0,0 +1,16 @@
1
+ import React, { RefObject } from "react";
2
+ export interface ScheduleSendPickerProps {
3
+ anchorRef: RefObject<HTMLElement | null>;
4
+ onClose: () => void;
5
+ /** Called with the chosen instant as a UTC ISO string, already validated to be in the future. */
6
+ onSchedule: (scheduledSendTimeIso: string) => void;
7
+ }
8
+ /**
9
+ * The "Send later" popover next to Compose's Send button — same `PopoverPortal` shell
10
+ * `EmojiPicker`/`GifPicker`/`ResourcePicker` already use, just holding a single `datetime-local` input
11
+ * instead of a grid/list. Validates the chosen time is genuinely in the future before calling
12
+ * `onSchedule` — `@rapidmx/restapi`'s own `send()` only defers when `scheduledSendTime` is strictly
13
+ * later than "now", so a past/immediate pick here would silently send right away instead of scheduling,
14
+ * which would surprise a reader expecting the picker's own choice to be honored.
15
+ */
16
+ export default function ScheduleSendPicker({ anchorRef, onClose, onSchedule }: ScheduleSendPickerProps): React.JSX.Element;
@@ -0,0 +1,27 @@
1
+ /** Saves a compose window's unsaved edits (or waits for a save already on the wire), resolving to `false` when
2
+ * something the window holds is still unsaved afterwards (any other value counts as saved). Never rejects. */
3
+ export type ComposeFlush = () => Promise<unknown>;
4
+ /**
5
+ * Marks the app as signing out (explicitly, or forced by a sign-out in another tab), so compose windows stop
6
+ * asking the browser to confirm leaving the page - the "Leave site?" prompt would otherwise let the user cancel
7
+ * the sign-out's own navigation and stay on a page whose session is being ended. Call it before flushing drafts
8
+ * and navigating. `clearSigningOut()` undoes it (a sign-out that didn't leave the page after all).
9
+ */
10
+ export declare function markSigningOut(): void;
11
+ /** Undoes `markSigningOut()`. */
12
+ export declare function clearSigningOut(): void;
13
+ /** Whether `markSigningOut()` has been called (and not cleared). */
14
+ export declare function isSigningOut(): boolean;
15
+ /**
16
+ * Registers an open compose window's draft flush, so leaving the app on purpose (Sign Out) can save the
17
+ * last edits still waiting on the autosave debounce before the session ends. Returns the unregister
18
+ * function. Module-level rather than React context: `AppShell` (the sign-out handler) renders the
19
+ * `ComposeProvider` itself, so it can't read that provider's context.
20
+ */
21
+ export declare function registerComposeFlush(flush: ComposeFlush): () => void;
22
+ /**
23
+ * Runs every registered flush and waits for all of them, but no longer than `timeoutMs`. Resolves to `true`
24
+ * only when every window reported its content saved in time (a window whose save failed also shows the user
25
+ * why, with Discard / Keep editing). Never rejects.
26
+ */
27
+ export declare function flushComposeDrafts(timeoutMs: number): Promise<boolean>;
@@ -0,0 +1,50 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ const flushers = new Set();
6
+ let signingOut = false;
7
+ /**
8
+ * Marks the app as signing out (explicitly, or forced by a sign-out in another tab), so compose windows stop
9
+ * asking the browser to confirm leaving the page - the "Leave site?" prompt would otherwise let the user cancel
10
+ * the sign-out's own navigation and stay on a page whose session is being ended. Call it before flushing drafts
11
+ * and navigating. `clearSigningOut()` undoes it (a sign-out that didn't leave the page after all).
12
+ */
13
+ export function markSigningOut() {
14
+ signingOut = true;
15
+ }
16
+ /** Undoes `markSigningOut()`. */
17
+ export function clearSigningOut() {
18
+ signingOut = false;
19
+ }
20
+ /** Whether `markSigningOut()` has been called (and not cleared). */
21
+ export function isSigningOut() {
22
+ return signingOut;
23
+ }
24
+ /**
25
+ * Registers an open compose window's draft flush, so leaving the app on purpose (Sign Out) can save the
26
+ * last edits still waiting on the autosave debounce before the session ends. Returns the unregister
27
+ * function. Module-level rather than React context: `AppShell` (the sign-out handler) renders the
28
+ * `ComposeProvider` itself, so it can't read that provider's context.
29
+ */
30
+ export function registerComposeFlush(flush) {
31
+ flushers.add(flush);
32
+ return () => {
33
+ flushers.delete(flush);
34
+ };
35
+ }
36
+ /**
37
+ * Runs every registered flush and waits for all of them, but no longer than `timeoutMs`. Resolves to `true`
38
+ * only when every window reported its content saved in time (a window whose save failed also shows the user
39
+ * why, with Discard / Keep editing). Never rejects.
40
+ */
41
+ export async function flushComposeDrafts(timeoutMs) {
42
+ let timer;
43
+ const timeout = new Promise((resolve) => {
44
+ timer = setTimeout(() => resolve(false), timeoutMs);
45
+ });
46
+ const all = Promise.all([...flushers].map((flush) => flush().catch(() => false))).then((results) => results.every((result) => result !== false));
47
+ const saved = await Promise.race([all, timeout]);
48
+ clearTimeout(timer);
49
+ return saved;
50
+ }
@@ -0,0 +1,57 @@
1
+ import React, { PropsWithChildren } from "react";
2
+ import { Folder, Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
3
+ import { AppShellProps } from "../../layout/AppShell.js";
4
+ export type MailShellProps = Omit<AppShellProps, "active">;
5
+ /** One mailbox's own mail folders (already filtered to `MAIL_FOLDER_TYPES` and unsorted) - one entry per
6
+ * mailbox in `mailboxes`, fetched in parallel so every accessible mailbox's folder tree can render
7
+ * simultaneously (see `MailShell`'s own doc comment on why this replaced the old single-mailbox `folders`
8
+ * field). `error` is set (and `folders` left empty) for a mailbox whose own `listFolders()` call failed -
9
+ * one mailbox's fetch failure must never blank out every other mailbox's section. */
10
+ export interface MailboxFolders {
11
+ mailbox: Mailbox;
12
+ folders: Folder[];
13
+ error?: string;
14
+ }
15
+ /** The well-known mail folder types an "aggregate" pseudo-folder can merge across every accessible
16
+ * mailbox - a fixed, small set: `outbox` is deliberately excluded (transient per-mailbox send-queue
17
+ * state, not a "merge across mailboxes" concept), and there's no aggregate concept for `calendar`/
18
+ * `contacts`/`tasks`/`notes`/`user` folders (Mail's own sidebar already excludes those - see
19
+ * `MAIL_FOLDER_TYPES`). */
20
+ export declare const AGGREGATE_FOLDER_TYPES: readonly ["inbox", "sent_items", "drafts", "deleted_items", "junk"];
21
+ export type AggregateFolderType = (typeof AGGREGATE_FOLDER_TYPES)[number];
22
+ export interface MailShellContextValue {
23
+ /** The mailbox currently selected (`?mailboxUid=`) - `undefined` while an aggregate pseudo-folder is
24
+ * active instead (`aggregateFolderType` set), since there's no single mailbox to speak of then. */
25
+ mailboxUid?: string;
26
+ /** The folder currently selected (`?folderUid=`, or the selected mailbox's Inbox) - `undefined` in
27
+ * aggregate mode, for the same reason. */
28
+ folderUid?: string;
29
+ /** Set only when an aggregate pseudo-folder (`?aggregate=inbox` etc.) is selected instead of a real,
30
+ * single mailbox+folder - mutually exclusive with `mailboxUid`/`folderUid` above. */
31
+ aggregateFolderType?: AggregateFolderType;
32
+ mailboxes: Mailbox[];
33
+ /** Every accessible mailbox's own mail folders - replaces the old single-mailbox `folders: Folder[]`
34
+ * now that every mailbox's tree renders at once (see `MailboxFolders`'s own doc comment). */
35
+ mailboxFolders: MailboxFolders[];
36
+ }
37
+ /** Reads the mailbox/folder a page is currently showing, as resolved by the enclosing `MailShell`. */
38
+ export declare function useMailShell(): MailShellContextValue;
39
+ /** Page size of the one `listMailboxes()` call this shell makes. */
40
+ export declare const MAILBOX_LIST_LIMIT = 100;
41
+ /**
42
+ * Mail's own contextual sidebar (every accessible mailbox's own folder tree, plus a merged "All Mailboxes"
43
+ * aggregate section) + content area, rendered inside the shared `AppShell` chrome (icon rail, header,
44
+ * impersonation banner — see that component). There is no client-side router in this framework (see
45
+ * `ReactRoute`'s file-convention resolver) — the selected mailbox/folder (or aggregate pseudo-folder) live
46
+ * in the URL's `?mailboxUid=`/`?folderUid=`/`?aggregate=` query params, read once on mount (never during
47
+ * the initial render itself, matching every other query-param reader in this codebase — e.g.
48
+ * `apps/admin/quarantine`'s `readMailboxUid()` — so the server-rendered and just-hydrated client markup
49
+ * match).
50
+ *
51
+ * Every accessible mailbox's folder tree renders at once - there is no "switch mailbox" affordance
52
+ * anymore (the mailbox `<select>` this shell used to have is gone) - so a single-mailbox user sees zero
53
+ * behavior change from before this, and a multi-mailbox user sees every mailbox's folders (and the new
54
+ * aggregate section) simultaneously, matching the shared-mailbox feature's own "display as a separate set
55
+ * of folders" requirement.
56
+ */
57
+ export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }: PropsWithChildren<MailShellProps>): React.JSX.Element;
@@ -57,6 +57,8 @@ function sortedFoldersOf(folders) {
57
57
  function aggregateUnreadCount(mailboxFolders, type) {
58
58
  return mailboxFolders.reduce((total, mf) => total + (mf.folders.find((f) => f.type === type)?.unreadCount ?? 0), 0);
59
59
  }
60
+ /** Page size of the one `listMailboxes()` call this shell makes. */
61
+ export const MAILBOX_LIST_LIMIT = 100;
60
62
  /**
61
63
  * A separate component (not inlined into `MailShell`'s own render) so `useCompose()` resolves against
62
64
  * `ComposeProvider` correctly: that provider is rendered *inside* the `AppShell` that `MailShell` itself
@@ -88,7 +90,7 @@ function ComposeButton() {
88
90
  * aggregate section) simultaneously, matching the shared-mailbox feature's own "display as a separate set
89
91
  * of folders" requirement.
90
92
  */
91
- export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, children, }) {
93
+ export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }) {
92
94
  const [status, setStatus] = useState("checking");
93
95
  const [error, setError] = useState(null);
94
96
  const [mailboxes, setMailboxes] = useState([]);
@@ -108,7 +110,7 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
108
110
  if (!userUid) {
109
111
  return;
110
112
  }
111
- listMailboxes({ limit: 100 })
113
+ listMailboxes({ limit: MAILBOX_LIST_LIMIT })
112
114
  .then((result) => {
113
115
  setMailboxes(result);
114
116
  setStatus("ready");
@@ -138,7 +140,7 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
138
140
  return;
139
141
  }
140
142
  setFoldersLoading(true);
141
- Promise.all(mailboxes.map((mailbox) => listFolders(mailbox.uid)
143
+ void Promise.all(mailboxes.map((mailbox) => listFolders(mailbox.uid)
142
144
  .then((result) => ({ mailbox, folders: result.filter((f) => MAIL_FOLDER_TYPES.has(f.type)) }))
143
145
  .catch((err) => ({
144
146
  mailbox,
@@ -210,5 +212,12 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
210
212
  // (ComposeWindow, MessageDetailPane) request an unlock on demand via useUnlockPrompt() instead.
211
213
  // First-time provisioning (a mailbox with no vault at all yet) still always blocks - see this
212
214
  // prop's own doc comment on KeyEnrollmentGateProps.
213
- blocking: false, children: [_jsx(LocalIndexLifecycle, { mailboxUid: activeMailboxUid, mailboxKeys: activeMailbox?.keys, folders: mailboxFolders.find((mf) => mf.mailbox.uid === activeMailboxUid)?.folders ?? [] }), _jsx(AppShell, { active: "mail", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, children: inner })] }));
215
+ blocking: false,
216
+ // A shared/delegated mailbox the caller doesn't own is never provisioned from here - its keys
217
+ // belong to its owner (or its admins), not to whoever happens to open it first - and neither is
218
+ // the impersonated user's own mailbox: an administrator must never choose its password/recovery codes.
219
+ canProvision: !impersonating && activeMailbox?.ownerUserUid === userUid, children: [_jsx(LocalIndexLifecycle, { mailboxUid: activeMailboxUid, mailboxKeys: activeMailbox?.keys, folders: mailboxFolders.find((mf) => mf.mailbox.uid === activeMailboxUid)?.folders ?? [],
220
+ // A full page may be truncated - pruning against it would delete indexes of accessible mailboxes
221
+ // beyond the limit, so it's only trusted as the complete list when it came back short.
222
+ accessibleMailboxUids: status === "ready" && mailboxes.length < MAILBOX_LIST_LIMIT ? mailboxes.map((mb) => mb.uid) : undefined }), _jsx(AppShell, { active: "mail", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, pluginNav: pluginNav, children: inner })] }));
214
223
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The trusted signing-key fingerprints for a received message's sender, for `evaluateMessageSecurity()`'s
3
+ * `pinnedSignerFingerprints`: the keys key discovery already pinned on the contacts in the reading mailbox's own
4
+ * contacts folder(s). Reads only stored contacts - never triggers a Discovery lookup, which must not happen on
5
+ * message receipt.
6
+ *
7
+ * `MessageDetailPane` renders once per opened message (and once per message in a thread), so each mailbox's
8
+ * contact list is fetched once and reused for `CONTACTS_CACHE_TTL_MS`; a failed load isn't cached. Any failure
9
+ * leaves the caller with no pins, which `evaluateMessageSecurity()` reports as an unverified signer - never as
10
+ * verified.
11
+ *
12
+ * The cache is dropped (`clearPinnedSignerCache()`) whenever a key session locks (subscribed on first use), on sign-out
13
+ * (`AppShell`), and after the reader adds, edits or deletes contacts (the contacts pages) - so a contact removed
14
+ * because its key was revoked doesn't keep vouching for signatures until the TTL runs out.
15
+ */
16
+ import { SignerKeyState } from "@rapidmx/react-shared/contacts/contactsApi.js";
17
+ /** How long one mailbox's loaded contact list is reused before it is fetched again. */
18
+ export declare const CONTACTS_CACHE_TTL_MS = 60000;
19
+ /** Forgets every cached contact list - on sign-out, a key-session lock, after the reader changes their contacts, and
20
+ * in tests. */
21
+ export declare function clearPinnedSignerCache(): void;
22
+ /** The sender `address`'s pinned signing fingerprints from `mailboxUid`'s contacts. Rejects if they can't be loaded. */
23
+ export declare function getPinnedSignerFingerprints(mailboxUid: string, address: string): Promise<string[]>;
24
+ /** A sender's signing-key state (`signerKeyStateFor()`) plus what a key-changed notice needs to show and link. */
25
+ export interface SenderKeyState extends SignerKeyState {
26
+ /** The contact holding the first pinned signing key, else the one holding the conflict, else the first match. */
27
+ contactUid?: string;
28
+ /** When the first pinned signing key started being trusted - see `keyPinnedSince()`. */
29
+ pinnedSince?: number;
30
+ }
31
+ /** The sender `address`'s signing-key state from `mailboxUid`'s (cached) contacts, for a "signing key changed" comparison
32
+ * or to spot a recorded conflict. Reads stored contacts only. Rejects if they can't be loaded. */
33
+ export declare function getSignerKeyState(mailboxUid: string, address: string): Promise<SenderKeyState>;
@@ -0,0 +1,98 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * The trusted signing-key fingerprints for a received message's sender, for `evaluateMessageSecurity()`'s
7
+ * `pinnedSignerFingerprints`: the keys key discovery already pinned on the contacts in the reading mailbox's own
8
+ * contacts folder(s). Reads only stored contacts - never triggers a Discovery lookup, which must not happen on
9
+ * message receipt.
10
+ *
11
+ * `MessageDetailPane` renders once per opened message (and once per message in a thread), so each mailbox's
12
+ * contact list is fetched once and reused for `CONTACTS_CACHE_TTL_MS`; a failed load isn't cached. Any failure
13
+ * leaves the caller with no pins, which `evaluateMessageSecurity()` reports as an unverified signer - never as
14
+ * verified.
15
+ *
16
+ * The cache is dropped (`clearPinnedSignerCache()`) whenever a key session locks (subscribed on first use), on sign-out
17
+ * (`AppShell`), and after the reader adds, edits or deletes contacts (the contacts pages) - so a contact removed
18
+ * because its key was revoked doesn't keep vouching for signatures until the TTL runs out.
19
+ */
20
+ import { PINNED_FINGERPRINT_MAX_PAGES, PINNED_FINGERPRINT_PAGE_SIZE, listContacts, pinnedSigningFingerprintsFor, signerKeyStateFor, } from "@rapidmx/react-shared/contacts/contactsApi.js";
21
+ import { keyPinnedSince } from "../contacts/contactKeys.js";
22
+ import { listFolders } from "@rapidmx/react-shared/mail/mailApi.js";
23
+ import { subscribeKeySession } from "@rapidmx/react-shared/crypto/keySession.js";
24
+ /** How long one mailbox's loaded contact list is reused before it is fetched again. */
25
+ export const CONTACTS_CACHE_TTL_MS = 60000;
26
+ const contactsCache = new Map();
27
+ /** Forgets every cached contact list - on sign-out, a key-session lock, after the reader changes their contacts, and
28
+ * in tests. */
29
+ export function clearPinnedSignerCache() {
30
+ contactsCache.clear();
31
+ }
32
+ let lockSubscribed = false;
33
+ /** Subscribes (once, lazily - a module-level subscription would run in every page that merely imports this) to key
34
+ * session changes, clearing the cache whenever any mailbox's keys are locked. */
35
+ function subscribeToLocksOnce() {
36
+ if (lockSubscribed) {
37
+ return;
38
+ }
39
+ lockSubscribed = true;
40
+ subscribeKeySession((event) => {
41
+ if (event.state === "locked") {
42
+ clearPinnedSignerCache();
43
+ }
44
+ });
45
+ }
46
+ async function loadMailboxContacts(mailboxUid) {
47
+ const folders = await listFolders(mailboxUid);
48
+ const contacts = [];
49
+ for (const folder of folders.filter((f) => f.type === "contacts")) {
50
+ for (let page = 0; page < PINNED_FINGERPRINT_MAX_PAGES; page++) {
51
+ const batch = await listContacts(folder.uid, { limit: PINNED_FINGERPRINT_PAGE_SIZE, page });
52
+ contacts.push(...batch);
53
+ if (batch.length < PINNED_FINGERPRINT_PAGE_SIZE) {
54
+ break;
55
+ }
56
+ }
57
+ }
58
+ return contacts;
59
+ }
60
+ /** `mailboxUid`'s contacts, from the cache when fresh. Rejects if they can't be loaded. */
61
+ function cachedMailboxContacts(mailboxUid) {
62
+ subscribeToLocksOnce();
63
+ let entry = contactsCache.get(mailboxUid);
64
+ if (!entry || Date.now() - entry.loadedAt > CONTACTS_CACHE_TTL_MS) {
65
+ const created = { loadedAt: Date.now(), contacts: loadMailboxContacts(mailboxUid) };
66
+ contactsCache.set(mailboxUid, created);
67
+ created.contacts.catch(() => {
68
+ if (contactsCache.get(mailboxUid) === created) {
69
+ contactsCache.delete(mailboxUid);
70
+ }
71
+ });
72
+ entry = created;
73
+ }
74
+ return entry.contacts;
75
+ }
76
+ /** The sender `address`'s pinned signing fingerprints from `mailboxUid`'s contacts. Rejects if they can't be loaded. */
77
+ export async function getPinnedSignerFingerprints(mailboxUid, address) {
78
+ return pinnedSigningFingerprintsFor(await cachedMailboxContacts(mailboxUid), address);
79
+ }
80
+ /** The sender `address`'s signing-key state from `mailboxUid`'s (cached) contacts, for a "signing key changed" comparison
81
+ * or to spot a recorded conflict. Reads stored contacts only. Rejects if they can't be loaded. */
82
+ export async function getSignerKeyState(mailboxUid, address) {
83
+ const contacts = await cachedMailboxContacts(mailboxUid);
84
+ const state = signerKeyStateFor(contacts, address);
85
+ const wanted = address.trim().toLowerCase();
86
+ const matching = contacts.filter((contact) => contact.emails.some((email) => email.address.trim().toLowerCase() === wanted));
87
+ // `signerKeyStateFor()` keeps the contacts' own key objects, so the holder is found by identity.
88
+ const pinned = state.pinned[0];
89
+ const holder = matching.find((candidate) => (candidate.keys ?? []).includes(pinned));
90
+ const contact = holder ??
91
+ matching.find((candidate) => (candidate.keyConflicts ?? []).some((conflict) => conflict.useType === "sign")) ??
92
+ matching[0];
93
+ return {
94
+ ...state,
95
+ ...(contact ? { contactUid: contact.uid } : {}),
96
+ ...(holder ? { pinnedSince: keyPinnedSince(holder, pinned) } : {}),
97
+ };
98
+ }
@@ -0,0 +1,22 @@
1
+ import { type Message } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ import type { SealToWrite } from "@rapidmx/react-shared/crypto/messageSecurity.js";
3
+ /** How long one mailbox's vault generation is reused before it is read again. */
4
+ export declare const VAULT_GENERATION_TTL_MS = 60000;
5
+ /** Forgets the cached vault generations and this session's seal writes - in tests. */
6
+ export declare function clearVerificationSealCache(): void;
7
+ /** Reads `mailboxUid`'s current vault `masterKeyGeneration`, uncached: `undefined` when the vault can't be read or
8
+ * reports none. Never rejects. */
9
+ export declare function readVaultGeneration(mailboxUid: string): Promise<number | undefined>;
10
+ /** `mailboxUid`'s current vault `masterKeyGeneration`, or `undefined` when it can't be read. Never rejects. */
11
+ export declare function getVaultGeneration(mailboxUid: string): Promise<number | undefined>;
12
+ /** The seal to check `message` against: the one this session stored for it when that is newer than the message's own
13
+ * copy (which may predate the write), else the message's `verificationSeal`/`verificationSealGeneration`. */
14
+ export declare function currentVerificationSeal(message: Pick<Message, "uid" | "verificationSeal" | "verificationSealGeneration">): {
15
+ seal?: string;
16
+ sealGeneration?: number;
17
+ };
18
+ /** Whether `sendVerificationSeal()` would still send a seal for `messageUid` at `generation`. */
19
+ export declare function verificationSealPending(messageUid: string, generation: number): boolean;
20
+ /** Stores `sealToWrite` on `messageUid`, best effort - see this module's doc comment. Resolves `true` when a request was
21
+ * sent, `false` when the write was skipped as already attempted. Never rejects. */
22
+ export declare function sendVerificationSeal(messageUid: string, sealToWrite: SealToWrite): Promise<boolean>;
@@ -0,0 +1,112 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * The web client's side of verification seals (`@rapidmx/react-shared`'s `crypto/verificationSeal.ts`): the vault's
7
+ * current `masterKeyGeneration` a seal is checked and written under, and best-effort seal writes.
8
+ *
9
+ * **Vault generation.** `getVaultGeneration()` reads `KeyVault.masterKeyGeneration` once per mailbox and reuses it for
10
+ * `VAULT_GENERATION_TTL_MS`; a failed read isn't cached. A vault that doesn't report a non-negative integer generation
11
+ * (a server predating it) counts as unavailable, and callers then evaluate without seals at all. The cache is dropped
12
+ * whenever a key session locks (subscribed on first use), so a rekey followed by a re-unlock is picked up.
13
+ *
14
+ * **Writes.** `sendVerificationSeal()` stores `sealToWrite` with `setMessageVerificationSeal()` and never rejects. Each
15
+ * message is written at most once per master key generation per page session - a seal carries the time it was built,
16
+ * so every evaluation of the same message produces a different one, and sending each would only draw 409s. A `409`
17
+ * (the server kept its seal), `400`, `403` or `404` is final; any other failure (a network error) lets a later
18
+ * evaluation try again, never a retry loop. A stored seal is remembered (`currentVerificationSeal()`) so a message
19
+ * reopened before its list copy is refreshed opens that seal instead of building another.
20
+ */
21
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
22
+ import { getKeyVault } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
23
+ import { subscribeKeySession } from "@rapidmx/react-shared/crypto/keySession.js";
24
+ import { setMessageVerificationSeal } from "@rapidmx/react-shared/mail/mailApi.js";
25
+ /** How long one mailbox's vault generation is reused before it is read again. */
26
+ export const VAULT_GENERATION_TTL_MS = 60000;
27
+ /** Statuses after which a seal write is never attempted again for that message and generation. */
28
+ const FINAL_WRITE_STATUSES = new Set([400, 403, 404, 409]);
29
+ const generationCache = new Map();
30
+ /** `messageUid:generation` for every write sent (or in flight) this session. */
31
+ const attemptedWrites = new Set();
32
+ /** Seals this session stored, by message uid. */
33
+ const storedSeals = new Map();
34
+ let lockSubscribed = false;
35
+ /** Forgets the cached vault generations and this session's seal writes - in tests. */
36
+ export function clearVerificationSealCache() {
37
+ generationCache.clear();
38
+ attemptedWrites.clear();
39
+ storedSeals.clear();
40
+ }
41
+ function subscribeToLocksOnce() {
42
+ if (lockSubscribed) {
43
+ return;
44
+ }
45
+ lockSubscribed = true;
46
+ subscribeKeySession((event) => {
47
+ if (event.state === "locked") {
48
+ generationCache.clear();
49
+ }
50
+ });
51
+ }
52
+ /** Reads `mailboxUid`'s current vault `masterKeyGeneration`, uncached: `undefined` when the vault can't be read or
53
+ * reports none. Never rejects. */
54
+ export async function readVaultGeneration(mailboxUid) {
55
+ try {
56
+ const { masterKeyGeneration } = await getKeyVault(mailboxUid);
57
+ return Number.isSafeInteger(masterKeyGeneration) && masterKeyGeneration >= 0 ? masterKeyGeneration : undefined;
58
+ }
59
+ catch {
60
+ return undefined;
61
+ }
62
+ }
63
+ /** `mailboxUid`'s current vault `masterKeyGeneration`, or `undefined` when it can't be read. Never rejects. */
64
+ export function getVaultGeneration(mailboxUid) {
65
+ subscribeToLocksOnce();
66
+ const entry = generationCache.get(mailboxUid);
67
+ if (entry && Date.now() - entry.loadedAt <= VAULT_GENERATION_TTL_MS) {
68
+ return entry.generation;
69
+ }
70
+ const created = { loadedAt: Date.now(), generation: readVaultGeneration(mailboxUid) };
71
+ generationCache.set(mailboxUid, created);
72
+ void created.generation.then((generation) => {
73
+ // Only a readable generation is reused; an unavailable one is read again next time.
74
+ if (generation === undefined && generationCache.get(mailboxUid) === created) {
75
+ generationCache.delete(mailboxUid);
76
+ }
77
+ });
78
+ return created.generation;
79
+ }
80
+ /** The seal to check `message` against: the one this session stored for it when that is newer than the message's own
81
+ * copy (which may predate the write), else the message's `verificationSeal`/`verificationSealGeneration`. */
82
+ export function currentVerificationSeal(message) {
83
+ const stored = storedSeals.get(message.uid);
84
+ if (stored && (message.verificationSealGeneration === undefined || stored.masterKeyGeneration > message.verificationSealGeneration)) {
85
+ return { seal: stored.seal, sealGeneration: stored.masterKeyGeneration };
86
+ }
87
+ return { seal: message.verificationSeal, sealGeneration: message.verificationSealGeneration };
88
+ }
89
+ /** Whether `sendVerificationSeal()` would still send a seal for `messageUid` at `generation`. */
90
+ export function verificationSealPending(messageUid, generation) {
91
+ return !attemptedWrites.has(`${messageUid}:${generation}`);
92
+ }
93
+ /** Stores `sealToWrite` on `messageUid`, best effort - see this module's doc comment. Resolves `true` when a request was
94
+ * sent, `false` when the write was skipped as already attempted. Never rejects. */
95
+ export async function sendVerificationSeal(messageUid, sealToWrite) {
96
+ const key = `${messageUid}:${sealToWrite.masterKeyGeneration}`;
97
+ if (attemptedWrites.has(key)) {
98
+ return false;
99
+ }
100
+ attemptedWrites.add(key);
101
+ try {
102
+ await setMessageVerificationSeal(messageUid, sealToWrite.seal, sealToWrite.masterKeyGeneration);
103
+ storedSeals.set(messageUid, sealToWrite);
104
+ }
105
+ catch (err) {
106
+ // `VerificationSealConflictError` is an `ApiRequestError` with status 409.
107
+ if (!(err instanceof ApiRequestError && FINAL_WRITE_STATUSES.has(err.status))) {
108
+ attemptedWrites.delete(key);
109
+ }
110
+ }
111
+ return true;
112
+ }
@@ -0,0 +1,15 @@
1
+ import type { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ /** `true` writable, `false` known view-only, `undefined` couldn't tell (treated as writable). */
3
+ export type MailboxWritability = boolean | undefined;
4
+ /** Forgets every cached answer - for tests, and anything that changes the caller's access mid-session. */
5
+ export declare function clearMailboxWritabilityCache(): void;
6
+ /** The already-known answer for `mailbox`, without making a request. */
7
+ export declare function peekMailboxWritability(mailbox: Mailbox, userUid: string | undefined, trusted?: boolean): MailboxWritability;
8
+ /** Whether the caller can create items in `mailbox`. Never rejects. */
9
+ export declare function getMailboxWritability(mailbox: Mailbox, userUid: string | undefined, trusted?: boolean): Promise<MailboxWritability>;
10
+ /** Per-mailbox writability answers for `mailboxes`, filled in as each one arrives (already-known answers are
11
+ * present from the first render). */
12
+ export declare function useMailboxWritability(mailboxes: Mailbox[], userUid: string | undefined, trusted?: boolean): Record<string, MailboxWritability>;
13
+ /** `mailboxes` minus any known to be view-only, always keeping `keepUid` (the picker's current/default value,
14
+ * so the control never shows a value missing from its own options). Lists everything until told otherwise. */
15
+ export declare function useWritableMailboxes(mailboxes: Mailbox[], userUid: string | undefined, keepUid?: string, trusted?: boolean): Mailbox[];