@rapidmx/web-client 0.3.1 → 0.5.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 (202) hide show
  1. package/apps/admin/audit-log/index.tsx +35 -8
  2. package/apps/admin/branding/index.tsx +39 -404
  3. package/apps/admin/data-requests/index.tsx +480 -485
  4. package/apps/admin/distribution-lists/[uid].tsx +19 -1
  5. package/apps/admin/domains/[uid].tsx +92 -259
  6. package/apps/admin/encryption-policy/index.tsx +19 -0
  7. package/apps/admin/escrow-scopes/[uid].tsx +348 -237
  8. package/apps/admin/escrow-scopes/new/index.tsx +8 -2
  9. package/apps/admin/index.tsx +118 -82
  10. package/apps/admin/ingest-queue/index.tsx +44 -5
  11. package/apps/admin/mailbox-policy/index.tsx +19 -0
  12. package/apps/admin/mailboxes/[uid].tsx +211 -184
  13. package/apps/admin/mailboxes/new/index.tsx +28 -291
  14. package/apps/admin/plugins/index.tsx +15 -0
  15. package/apps/admin/quarantine/index.tsx +46 -14
  16. package/apps/admin/retention-policy/index.tsx +39 -132
  17. package/apps/admin/setup/index.tsx +15 -0
  18. package/apps/admin/transport-rules/[uid].tsx +30 -1
  19. package/apps/admin/transport-rules/_transportRuleConfig.tsx +38 -2
  20. package/apps/admin/transport-rules/index.tsx +24 -6
  21. package/apps/admin/transport-rules/new/index.tsx +30 -1
  22. package/apps/escrow/_layout.tsx +2 -3
  23. package/apps/escrow/audit-log/index.tsx +196 -168
  24. package/apps/escrow/matters/[uid].tsx +621 -521
  25. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +22 -1
  26. package/apps/shared/components/admin/layout/AdminShell.tsx +251 -210
  27. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -0
  28. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  29. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +55 -11
  30. package/apps/shared/components/admin/settings/BrandingForm.tsx +385 -0
  31. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +187 -0
  32. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +112 -0
  33. package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +34 -0
  34. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +308 -0
  35. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +152 -0
  36. package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -0
  37. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -0
  38. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +290 -0
  39. package/apps/shared/components/admin/setup/SetupWizard.tsx +416 -0
  40. package/apps/shared/components/admin/signOut.ts +79 -0
  41. package/apps/shared/components/admin/usePagedList.tsx +129 -0
  42. package/apps/shared/components/calendar/CalendarListSidebar.tsx +120 -76
  43. package/apps/shared/components/calendar/EventModal.tsx +717 -513
  44. package/apps/shared/components/calendar/MonthView.tsx +173 -139
  45. package/apps/shared/components/calendar/RecurrenceEditor.tsx +222 -180
  46. package/apps/shared/components/calendar/SplitDayView.tsx +143 -137
  47. package/apps/shared/components/calendar/TimeGridView.tsx +244 -198
  48. package/apps/shared/components/calendar/allDay.ts +124 -0
  49. package/apps/shared/components/calendar/layout/CalendarShell.tsx +80 -99
  50. package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
  51. package/apps/shared/components/contacts/ContactForm.tsx +377 -314
  52. package/apps/shared/components/contacts/ContactsSidebar.tsx +6 -2
  53. package/apps/shared/components/contacts/KeyChangeReview.tsx +192 -0
  54. package/apps/shared/components/contacts/contactKeys.ts +78 -0
  55. package/apps/shared/components/escrow/layout/EscrowShell.tsx +148 -134
  56. package/apps/shared/components/layout/AppShell.tsx +270 -169
  57. package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
  58. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +150 -26
  59. package/apps/shared/components/layout/MailboxProvisioning.tsx +19 -3
  60. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +350 -0
  61. package/apps/shared/components/layout/UnlockPromptProvider.tsx +257 -0
  62. package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
  63. package/apps/shared/components/mail/MessageDetailPane.tsx +799 -76
  64. package/apps/shared/components/mail/compose/ComposeContext.tsx +26 -16
  65. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1652 -765
  66. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
  67. package/apps/shared/components/mail/layout/MailShell.tsx +418 -302
  68. package/apps/shared/components/mail/pinnedSigners.ts +124 -0
  69. package/apps/shared/components/mail/verificationSeals.ts +125 -0
  70. package/apps/shared/components/mail/writableMailboxes.ts +101 -0
  71. package/apps/shared/components/rules/RuleBuilder.tsx +311 -276
  72. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
  73. package/apps/shared/mail/findWellKnownFolderUid.ts +13 -0
  74. package/apps/shared/mail/listAllPages.ts +39 -0
  75. package/apps/shared/search/LocalIndexLifecycle.tsx +98 -0
  76. package/apps/shared/search/localIndexBlockCipher.ts +83 -0
  77. package/apps/shared/search/localIndexBuilder.ts +481 -0
  78. package/apps/shared/search/localIndexKey.ts +27 -0
  79. package/apps/shared/search/localIndexRpcClient.ts +316 -0
  80. package/apps/shared/search/localIndexSchema.ts +236 -0
  81. package/apps/shared/search/localIndexSizePreference.ts +88 -0
  82. package/apps/shared/search/localIndexStorage.ts +98 -0
  83. package/apps/shared/search/localIndexVFS.ts +266 -0
  84. package/apps/shared/search/localIndexWorker.ts +926 -0
  85. package/apps/shared/search/searchTier2.ts +79 -0
  86. package/apps/shared/search/wa-sqlite-shims.d.ts +44 -0
  87. package/apps/www/calendar/index.tsx +438 -407
  88. package/apps/www/contacts/[uid].tsx +21 -2
  89. package/apps/www/contacts/index.tsx +148 -17
  90. package/apps/www/index.tsx +1314 -486
  91. package/apps/www/messages/[uid].tsx +23 -7
  92. package/apps/www/settings/auto-reply/index.tsx +134 -129
  93. package/apps/www/settings/booking-types/[uid].tsx +306 -300
  94. package/apps/www/settings/encryption/index.tsx +800 -215
  95. package/apps/www/settings/filters/[uid].tsx +171 -155
  96. package/apps/www/settings/filters/new/index.tsx +8 -1
  97. package/apps/www/settings/focused-inbox/index.tsx +150 -148
  98. package/apps/www/settings/privacy/index.tsx +494 -436
  99. package/apps/www/settings/read-receipts/index.tsx +148 -125
  100. package/apps/www/settings/sharing/index.tsx +274 -0
  101. package/apps/www/tasks/index.tsx +638 -530
  102. package/dist/apps/admin/audit-log/index.js +38 -8
  103. package/dist/apps/admin/branding/index.js +4 -98
  104. package/dist/apps/admin/data-requests/index.js +52 -72
  105. package/dist/apps/admin/distribution-lists/[uid].js +1 -1
  106. package/dist/apps/admin/domains/[uid].js +5 -68
  107. package/dist/apps/admin/encryption-policy/index.js +8 -0
  108. package/dist/apps/admin/escrow-scopes/[uid].js +91 -12
  109. package/dist/apps/admin/escrow-scopes/new/index.js +8 -3
  110. package/dist/apps/admin/index.js +17 -1
  111. package/dist/apps/admin/ingest-queue/index.js +24 -6
  112. package/dist/apps/admin/mailbox-policy/index.js +8 -0
  113. package/dist/apps/admin/mailboxes/[uid].js +20 -4
  114. package/dist/apps/admin/mailboxes/new/index.js +4 -89
  115. package/dist/apps/admin/plugins/index.js +6 -0
  116. package/dist/apps/admin/quarantine/index.js +20 -11
  117. package/dist/apps/admin/retention-policy/index.js +3 -36
  118. package/dist/apps/admin/setup/index.js +6 -0
  119. package/dist/apps/admin/transport-rules/[uid].js +18 -2
  120. package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
  121. package/dist/apps/admin/transport-rules/index.js +22 -6
  122. package/dist/apps/admin/transport-rules/new/index.js +18 -2
  123. package/dist/apps/escrow/_layout.js +3 -3
  124. package/dist/apps/escrow/audit-log/index.js +26 -1
  125. package/dist/apps/escrow/matters/[uid].js +77 -42
  126. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
  127. package/dist/apps/shared/components/admin/layout/AdminShell.js +37 -4
  128. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -0
  129. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  130. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
  131. package/dist/apps/shared/components/admin/settings/BrandingForm.js +116 -0
  132. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +83 -0
  133. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +62 -0
  134. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +25 -0
  135. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +112 -0
  136. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +74 -0
  137. package/dist/apps/shared/components/admin/settings/PluginsManager.js +572 -0
  138. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +73 -0
  139. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +147 -0
  140. package/dist/apps/shared/components/admin/setup/SetupWizard.js +208 -0
  141. package/dist/apps/shared/components/admin/signOut.js +75 -0
  142. package/dist/apps/shared/components/admin/usePagedList.js +99 -0
  143. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +24 -13
  144. package/dist/apps/shared/components/calendar/EventModal.js +154 -21
  145. package/dist/apps/shared/components/calendar/MonthView.js +12 -5
  146. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
  147. package/dist/apps/shared/components/calendar/SplitDayView.js +10 -4
  148. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
  149. package/dist/apps/shared/components/calendar/allDay.js +111 -0
  150. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +47 -39
  151. package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
  152. package/dist/apps/shared/components/contacts/ContactForm.js +45 -12
  153. package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
  154. package/dist/apps/shared/components/contacts/KeyChangeReview.js +63 -0
  155. package/dist/apps/shared/components/contacts/contactKeys.js +64 -0
  156. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +24 -5
  157. package/dist/apps/shared/components/layout/AppShell.js +100 -8
  158. package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
  159. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +63 -14
  160. package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
  161. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
  162. package/dist/apps/shared/components/layout/UnlockPromptProvider.js +136 -0
  163. package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
  164. package/dist/apps/shared/components/mail/MessageDetailPane.js +464 -34
  165. package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
  166. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +807 -116
  167. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
  168. package/dist/apps/shared/components/mail/layout/MailShell.js +116 -41
  169. package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
  170. package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
  171. package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
  172. package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
  173. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
  174. package/dist/apps/shared/mail/findWellKnownFolderUid.js +12 -0
  175. package/dist/apps/shared/mail/listAllPages.js +25 -0
  176. package/dist/apps/shared/search/LocalIndexLifecycle.js +80 -0
  177. package/dist/apps/shared/search/localIndexBlockCipher.js +63 -0
  178. package/dist/apps/shared/search/localIndexBuilder.js +408 -0
  179. package/dist/apps/shared/search/localIndexKey.js +25 -0
  180. package/dist/apps/shared/search/localIndexRpcClient.js +241 -0
  181. package/dist/apps/shared/search/localIndexSchema.js +185 -0
  182. package/dist/apps/shared/search/localIndexSizePreference.js +78 -0
  183. package/dist/apps/shared/search/localIndexStorage.js +86 -0
  184. package/dist/apps/shared/search/localIndexVFS.js +258 -0
  185. package/dist/apps/shared/search/localIndexWorker.js +687 -0
  186. package/dist/apps/shared/search/searchTier2.js +38 -0
  187. package/dist/apps/www/calendar/index.js +38 -18
  188. package/dist/apps/www/contacts/[uid].js +13 -2
  189. package/dist/apps/www/contacts/index.js +94 -15
  190. package/dist/apps/www/index.js +770 -137
  191. package/dist/apps/www/messages/[uid].js +23 -7
  192. package/dist/apps/www/settings/auto-reply/index.js +9 -4
  193. package/dist/apps/www/settings/booking-types/[uid].js +17 -12
  194. package/dist/apps/www/settings/encryption/index.js +541 -96
  195. package/dist/apps/www/settings/filters/[uid].js +22 -7
  196. package/dist/apps/www/settings/filters/new/index.js +8 -2
  197. package/dist/apps/www/settings/focused-inbox/index.js +3 -1
  198. package/dist/apps/www/settings/privacy/index.js +57 -22
  199. package/dist/apps/www/settings/read-receipts/index.js +10 -3
  200. package/dist/apps/www/settings/sharing/index.js +124 -0
  201. package/dist/apps/www/tasks/index.js +76 -14
  202. package/package.json +3 -2
@@ -1,184 +1,211 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import React, { useEffect, useState } from "react";
6
- import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
- import { deleteMailbox, getMailbox, impersonateUser, Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
8
- import AdminShell, { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
9
- import ShareAccessCard from "../../shared/components/admin/mailboxes/ShareAccessCard.js";
10
- import ResourceSettingsCard from "../../shared/components/admin/mailboxes/ResourceSettingsCard.js";
11
- import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
12
- import Button from "@rapidmx/react-shared/components/buttons/Button.js";
13
- import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
14
-
15
- function formatBytes(bytes: number): string {
16
- if (bytes >= 1_000_000_000) return `${(bytes / 1_000_000_000).toFixed(2)} GB`;
17
- if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB`;
18
- if (bytes >= 1_000) return `${(bytes / 1_000).toFixed(1)} KB`;
19
- return `${bytes} B`;
20
- }
21
-
22
- export default function MailboxDetailPage(props: Omit<AdminShellProps, "active"> & { params: { uid: string } }) {
23
- return (
24
- <AdminShell {...props} active="mailboxes">
25
- <MailboxDetailContent uid={props.params.uid} impersonationBaseUrl={props.impersonationBaseUrl} />
26
- </AdminShell>
27
- );
28
- }
29
-
30
- function MailboxDetailContent({ uid, impersonationBaseUrl }: { uid: string } & Pick<AdminShellProps, "impersonationBaseUrl">) {
31
- const [mailbox, setMailbox] = useState<Mailbox | null>(null);
32
- const [loading, setLoading] = useState(true);
33
- const [error, setError] = useState<string | null>(null);
34
- const [impersonating, setImpersonating] = useState(false);
35
- const [confirmingDelete, setConfirmingDelete] = useState(false);
36
- const [deleting, setDeleting] = useState(false);
37
- const [deleteError, setDeleteError] = useState<string | null>(null);
38
-
39
- async function handleAccessMailbox(ownerUserUid: string) {
40
- setImpersonating(true);
41
- try {
42
- await impersonateUser(impersonationBaseUrl ?? "", ownerUserUid);
43
- window.location.href = "/";
44
- } catch (err) {
45
- setError(err instanceof ApiRequestError ? err.message : "Could not access this mailbox.");
46
- setImpersonating(false);
47
- }
48
- }
49
-
50
- function closeDeleteModal() {
51
- setConfirmingDelete(false);
52
- }
53
-
54
- // Only ever invoked from the delete-confirmation modal below, which itself only renders once
55
- // `mailbox` is resolved (the `error || !mailbox` branch below returns before this content mounts).
56
- async function handleDelete() {
57
- setDeleting(true);
58
- setDeleteError(null);
59
- try {
60
- await deleteMailbox(mailbox!.uid, mailbox!.version);
61
- window.location.href = "/admin";
62
- } catch (err) {
63
- // Most commonly a 409 if this mailbox is a custodian on an open legal hold (restapi's own
64
- // `assertNotOnLegalHold()`, naming the blocking Matter uid(s)) - surfaced as-is, same as
65
- // every other destructive-action error in this codebase, rather than special-cased here.
66
- setDeleteError(err instanceof ApiRequestError ? err.message : "Could not delete this mailbox.");
67
- setDeleting(false);
68
- }
69
- }
70
-
71
- useEffect(() => {
72
- setLoading(true);
73
- setError(null);
74
- getMailbox(uid)
75
- .then(setMailbox)
76
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this mailbox."))
77
- .finally(() => setLoading(false));
78
- }, [uid]);
79
-
80
- if (loading) {
81
- return <p className="text-sm text-text-muted">Loading&hellip;</p>;
82
- }
83
- if (error || !mailbox) {
84
- return <Alert>{error ?? "Mailbox not found."}</Alert>;
85
- }
86
-
87
- const quarantineHref = `/admin/quarantine?mailboxUid=${encodeURIComponent(mailbox.uid)}`;
88
- const ingestQueueHref = `/admin/ingest-queue?mailboxUid=${encodeURIComponent(mailbox.uid)}`;
89
-
90
- return (
91
- <div className="max-w-3xl flex flex-col gap-5">
92
- <div className="flex items-start justify-between gap-4">
93
- <div>
94
- <a href="/admin" className="text-sm text-primary-dark hover:underline">
95
- &larr; All mailboxes
96
- </a>
97
- <h1 className="text-xl font-bold tracking-tight mt-1">{mailbox.primarySmtpAddress}</h1>
98
- </div>
99
- <div className="flex gap-3 shrink-0">
100
- {mailbox.ownerUserUid && (
101
- <Button
102
- type="button"
103
- variant="secondary"
104
- className="!w-auto"
105
- loading={impersonating}
106
- disabled={impersonating}
107
- onClick={() => handleAccessMailbox(mailbox.ownerUserUid as string)}
108
- >
109
- Access this mailbox
110
- </Button>
111
- )}
112
- <Button
113
- type="button"
114
- variant="secondary"
115
- className="!w-auto !border-danger !text-danger hover:!border-danger hover:!text-danger"
116
- onClick={() => setConfirmingDelete(true)}
117
- >
118
- Delete mailbox
119
- </Button>
120
- </div>
121
- </div>
122
-
123
- <div className="bg-surface border border-border rounded-md p-6">
124
- <dl className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm">
125
- <dt className="text-text-muted">Display name</dt>
126
- <dd>{mailbox.displayName}</dd>
127
- <dt className="text-text-muted">Owner</dt>
128
- <dd>{mailbox.ownerUserUid ?? "None (shared mailbox)"}</dd>
129
- <dt className="text-text-muted">Timezone</dt>
130
- <dd>{mailbox.timezone}</dd>
131
- <dt className="text-text-muted">Quota</dt>
132
- <dd>
133
- {formatBytes(mailbox.usedBytes)} / {formatBytes(mailbox.quotaBytes)}
134
- </dd>
135
- <dt className="text-text-muted">Alias addresses</dt>
136
- <dd>{mailbox.aliasAddresses.length > 0 ? mailbox.aliasAddresses.join(", ") : "None"}</dd>
137
- {mailbox.isResource && (
138
- <>
139
- <dt className="text-text-muted">Resource type</dt>
140
- <dd className="capitalize">{mailbox.resourceType ?? "room"}</dd>
141
- </>
142
- )}
143
- <dt className="text-text-muted">Created</dt>
144
- <dd>{new Date(mailbox.dateCreated).toLocaleString()}</dd>
145
- </dl>
146
- <div className="flex gap-4 mt-5 pt-5 border-t border-border text-sm font-medium">
147
- <a href={quarantineHref} className="text-primary-dark hover:underline">
148
- View quarantine
149
- </a>
150
- <a href={ingestQueueHref} className="text-primary-dark hover:underline">
151
- View ingest queue
152
- </a>
153
- </div>
154
- </div>
155
-
156
- <ShareAccessCard mailboxUid={mailbox.uid} />
157
-
158
- {mailbox.isResource && <ResourceSettingsCard mailbox={mailbox} onUpdate={setMailbox} />}
159
-
160
- <Modal open={confirmingDelete} onClose={closeDeleteModal} title="Delete mailbox">
161
- <p className="text-sm mb-5">
162
- Are you sure you want to delete <strong>{mailbox.primarySmtpAddress}</strong>? This permanently
163
- deletes the mailbox and everything in it, and cannot be undone. It fails if this mailbox is a
164
- custodian on an active legal hold.
165
- </p>
166
- {deleteError && <Alert>{deleteError}</Alert>}
167
- <div className="flex gap-3 justify-end mt-5">
168
- <Button type="button" variant="secondary" className="!w-auto" disabled={deleting} onClick={closeDeleteModal}>
169
- Cancel
170
- </Button>
171
- <Button
172
- type="button"
173
- className="!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger"
174
- loading={deleting}
175
- disabled={deleting}
176
- onClick={handleDelete}
177
- >
178
- Delete
179
- </Button>
180
- </div>
181
- </Modal>
182
- </div>
183
- );
184
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { useEffect, useState } from "react";
6
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
+ import { deleteMailbox, getMailbox, impersonateUser, Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
8
+ import AdminShell, { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
9
+ import ShareAccessCard from "../../shared/components/admin/mailboxes/ShareAccessCard.js";
10
+ import ResourceSettingsCard from "../../shared/components/admin/mailboxes/ResourceSettingsCard.js";
11
+ import EscrowScopeCard from "../../shared/components/admin/mailboxes/EscrowScopeCard.js";
12
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
13
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
14
+ import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
15
+
16
+ function formatBytes(bytes: number): string {
17
+ if (bytes >= 1_000_000_000) return `${(bytes / 1_000_000_000).toFixed(2)} GB`;
18
+ if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB`;
19
+ if (bytes >= 1_000) return `${(bytes / 1_000).toFixed(1)} KB`;
20
+ return `${bytes} B`;
21
+ }
22
+
23
+ export default function MailboxDetailPage(props: Omit<AdminShellProps, "active"> & { params: { uid: string } }) {
24
+ return (
25
+ <AdminShell {...props} active="mailboxes">
26
+ <MailboxDetailContent uid={props.params.uid} impersonationBaseUrl={props.impersonationBaseUrl} />
27
+ </AdminShell>
28
+ );
29
+ }
30
+
31
+ function MailboxDetailContent({ uid, impersonationBaseUrl }: { uid: string } & Pick<AdminShellProps, "impersonationBaseUrl">) {
32
+ const [mailbox, setMailbox] = useState<Mailbox | null>(null);
33
+ const [loading, setLoading] = useState(true);
34
+ const [error, setError] = useState<string | null>(null);
35
+ const [confirmingAccess, setConfirmingAccess] = useState(false);
36
+ const [impersonating, setImpersonating] = useState(false);
37
+ const [accessError, setAccessError] = useState<string | null>(null);
38
+ const [confirmingDelete, setConfirmingDelete] = useState(false);
39
+ const [deleting, setDeleting] = useState(false);
40
+ const [deleteError, setDeleteError] = useState<string | null>(null);
41
+
42
+ // Ignored while the action is under way (Escape/Close; Cancel is disabled then too), so its result or error
43
+ // can't land on a modal reopened afterwards.
44
+ function closeAccessModal() {
45
+ if (impersonating) return;
46
+ setConfirmingAccess(false);
47
+ setAccessError(null);
48
+ }
49
+
50
+ // Only ever invoked from the access-confirmation modal below, which only renders for a loaded mailbox that has
51
+ // an owner. A failure stays in the modal rather than replacing the whole page.
52
+ async function handleAccessMailbox() {
53
+ setImpersonating(true);
54
+ setAccessError(null);
55
+ try {
56
+ await impersonateUser(impersonationBaseUrl ?? "", mailbox!.ownerUserUid!);
57
+ window.location.href = "/";
58
+ } catch (err) {
59
+ setAccessError(err instanceof ApiRequestError ? err.message : "Could not access this mailbox.");
60
+ setImpersonating(false);
61
+ }
62
+ }
63
+
64
+ function closeDeleteModal() {
65
+ if (deleting) return;
66
+ setConfirmingDelete(false);
67
+ }
68
+
69
+ // Only ever invoked from the delete-confirmation modal below, which itself only renders once
70
+ // `mailbox` is resolved (the `error || !mailbox` branch below returns before this content mounts).
71
+ async function handleDelete() {
72
+ setDeleting(true);
73
+ setDeleteError(null);
74
+ try {
75
+ await deleteMailbox(mailbox!.uid, mailbox!.version);
76
+ window.location.href = "/admin";
77
+ } catch (err) {
78
+ // Most commonly a 409 if this mailbox is a custodian on an open legal hold (restapi's own
79
+ // `assertNotOnLegalHold()`, naming the blocking Matter uid(s)) - surfaced as-is, same as
80
+ // every other destructive-action error in this codebase, rather than special-cased here.
81
+ setDeleteError(err instanceof ApiRequestError ? err.message : "Could not delete this mailbox.");
82
+ setDeleting(false);
83
+ }
84
+ }
85
+
86
+ useEffect(() => {
87
+ setLoading(true);
88
+ setError(null);
89
+ getMailbox(uid)
90
+ .then(setMailbox)
91
+ .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this mailbox."))
92
+ .finally(() => setLoading(false));
93
+ }, [uid]);
94
+
95
+ if (loading) {
96
+ return <p className="text-sm text-text-muted">Loading&hellip;</p>;
97
+ }
98
+ if (error || !mailbox) {
99
+ return <Alert>{error ?? "Mailbox not found."}</Alert>;
100
+ }
101
+
102
+ const quarantineHref = `/admin/quarantine?mailboxUid=${encodeURIComponent(mailbox.uid)}`;
103
+ const ingestQueueHref = `/admin/ingest-queue?mailboxUid=${encodeURIComponent(mailbox.uid)}`;
104
+
105
+ return (
106
+ <div className="max-w-3xl flex flex-col gap-5">
107
+ <div className="flex items-start justify-between gap-4">
108
+ <div>
109
+ <a href="/admin" className="text-sm text-primary-dark hover:underline">
110
+ &larr; All mailboxes
111
+ </a>
112
+ <h1 className="text-xl font-bold tracking-tight mt-1">{mailbox.primarySmtpAddress}</h1>
113
+ </div>
114
+ <div className="flex gap-3 shrink-0">
115
+ {mailbox.ownerUserUid && (
116
+ <Button type="button" variant="secondary" className="!w-auto" onClick={() => setConfirmingAccess(true)}>
117
+ Access this mailbox
118
+ </Button>
119
+ )}
120
+ <Button
121
+ type="button"
122
+ variant="secondary"
123
+ className="!w-auto !border-danger !text-danger hover:!border-danger hover:!text-danger"
124
+ onClick={() => setConfirmingDelete(true)}
125
+ >
126
+ Delete mailbox
127
+ </Button>
128
+ </div>
129
+ </div>
130
+
131
+ <div className="bg-surface border border-border rounded-md p-6">
132
+ <dl className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm">
133
+ <dt className="text-text-muted">Display name</dt>
134
+ <dd>{mailbox.displayName}</dd>
135
+ <dt className="text-text-muted">Owner</dt>
136
+ <dd>{mailbox.ownerUserUid ?? "None (shared mailbox)"}</dd>
137
+ <dt className="text-text-muted">Timezone</dt>
138
+ <dd>{mailbox.timezone}</dd>
139
+ <dt className="text-text-muted">Quota</dt>
140
+ <dd>
141
+ {formatBytes(mailbox.usedBytes)} / {formatBytes(mailbox.quotaBytes)}
142
+ </dd>
143
+ <dt className="text-text-muted">Alias addresses</dt>
144
+ <dd>{mailbox.aliasAddresses.length > 0 ? mailbox.aliasAddresses.join(", ") : "None"}</dd>
145
+ {mailbox.isResource && (
146
+ <>
147
+ <dt className="text-text-muted">Resource type</dt>
148
+ <dd className="capitalize">{mailbox.resourceType ?? "room"}</dd>
149
+ </>
150
+ )}
151
+ <dt className="text-text-muted">Created</dt>
152
+ <dd>{new Date(mailbox.dateCreated).toLocaleString()}</dd>
153
+ </dl>
154
+ <div className="flex gap-4 mt-5 pt-5 border-t border-border text-sm font-medium">
155
+ <a href={quarantineHref} className="text-primary-dark hover:underline">
156
+ View quarantine
157
+ </a>
158
+ <a href={ingestQueueHref} className="text-primary-dark hover:underline">
159
+ View ingest queue
160
+ </a>
161
+ </div>
162
+ </div>
163
+
164
+ <ShareAccessCard mailboxUid={mailbox.uid} ownerUserUid={mailbox.ownerUserUid} />
165
+
166
+ <EscrowScopeCard mailbox={mailbox} onUpdate={setMailbox} />
167
+
168
+ {mailbox.isResource && <ResourceSettingsCard mailbox={mailbox} onUpdate={setMailbox} />}
169
+
170
+ <Modal open={confirmingAccess} onClose={closeAccessModal} title="Access this mailbox">
171
+ <p className="text-sm mb-3">
172
+ You&rsquo;ll be signed in as <strong className="break-all">{mailbox.ownerUserUid}</strong>, the owner
173
+ of <strong className="break-all">{mailbox.primarySmtpAddress}</strong>, and see everything they can -
174
+ and act as them, until you stop impersonating.
175
+ </p>
176
+ {accessError && <Alert>{accessError}</Alert>}
177
+ <div className="flex gap-3 justify-end mt-5">
178
+ <Button type="button" variant="secondary" className="!w-auto" disabled={impersonating} onClick={closeAccessModal}>
179
+ Cancel
180
+ </Button>
181
+ <Button type="button" className="!w-auto" loading={impersonating} disabled={impersonating} onClick={handleAccessMailbox}>
182
+ Access mailbox
183
+ </Button>
184
+ </div>
185
+ </Modal>
186
+
187
+ <Modal open={confirmingDelete} onClose={closeDeleteModal} title="Delete mailbox">
188
+ <p className="text-sm mb-5">
189
+ Are you sure you want to delete <strong>{mailbox.primarySmtpAddress}</strong>? This permanently
190
+ deletes the mailbox and everything in it, and cannot be undone. It fails if this mailbox is a
191
+ custodian on an active legal hold.
192
+ </p>
193
+ {deleteError && <Alert>{deleteError}</Alert>}
194
+ <div className="flex gap-3 justify-end mt-5">
195
+ <Button type="button" variant="secondary" className="!w-auto" disabled={deleting} onClick={closeDeleteModal}>
196
+ Cancel
197
+ </Button>
198
+ <Button
199
+ type="button"
200
+ className="!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger"
201
+ loading={deleting}
202
+ disabled={deleting}
203
+ onClick={handleDelete}
204
+ >
205
+ Delete
206
+ </Button>
207
+ </div>
208
+ </Modal>
209
+ </div>
210
+ );
211
+ }