@rapidmx/web-client 0.35.0 → 0.37.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.
- package/apps/escrow/matters/[uid].tsx +4 -3
- package/apps/shared/calendar/calendarReminders.ts +35 -0
- package/apps/shared/calendar/sealedEvents.ts +113 -0
- package/apps/shared/calendar/useCalendarReminders.ts +28 -9
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -0
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +89 -3
- package/apps/shared/components/calendar/EventDetails.tsx +13 -2
- package/apps/shared/components/calendar/EventEditor.tsx +124 -17
- package/apps/shared/components/calendar/EventFormParts.tsx +1 -0
- package/apps/shared/components/calendar/EventModal.tsx +19 -2
- package/apps/shared/components/calendar/RequestChangeForm.tsx +4 -1
- package/apps/shared/components/calendar/eventForm.ts +2 -0
- package/apps/shared/components/contacts/ContactCard.tsx +13 -1
- package/apps/shared/components/contacts/ContactDetailPane.tsx +189 -9
- package/apps/shared/components/contacts/ContactForm.tsx +56 -7
- package/apps/shared/components/contacts/KeyChangeReview.tsx +5 -1
- package/apps/shared/components/contacts/contactCardData.ts +8 -4
- package/apps/shared/components/contacts/contactKeys.ts +3 -3
- package/apps/shared/components/layout/AppShell.tsx +34 -5
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +30 -1
- package/apps/shared/components/layout/SharedMailboxKeyManager.tsx +122 -0
- package/apps/shared/components/layout/UnlockPromptProvider.tsx +34 -0
- package/apps/shared/components/mail/ConversationList.tsx +1 -1
- package/apps/shared/components/mail/InviteCard.tsx +11 -1
- package/apps/shared/components/mail/MessageDetailPane.tsx +59 -26
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +9 -2
- package/apps/shared/components/mail/compose/RecipientInput.tsx +2 -1
- package/apps/shared/components/mail/compose/draftResume.ts +28 -3
- package/apps/shared/components/mail/compose/protectedContactSuggestions.ts +121 -0
- package/apps/shared/components/mail/compose/quotedBody.ts +4 -3
- package/apps/shared/components/mail/invite/inviteStore.ts +17 -1
- package/apps/shared/components/mail/layout/MailShell.tsx +2 -0
- package/apps/shared/components/mail/pinnedSigners.ts +14 -0
- package/apps/shared/components/mail/reading/conversationPrint.ts +3 -3
- package/apps/shared/components/mail/reading/useMessageActions.ts +4 -1
- package/apps/shared/components/settings/KeyExportSetting.tsx +173 -0
- package/apps/shared/components/settings/OpenPgpKeySetting.tsx +240 -0
- package/apps/shared/components/settings/PolicyLockedNotice.tsx +23 -0
- package/apps/shared/components/settings/ZeroAccessSetting.tsx +128 -0
- package/apps/shared/components/settings/ZeroAccessTypes.tsx +175 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +9 -2
- package/apps/shared/mail/decryptedMessages.ts +11 -3
- package/apps/shared/mail/outbox/sendJob.ts +349 -50
- package/apps/shared/mail/outbox/sendOutcomes.ts +91 -20
- package/apps/shared/mail/reportNotices.ts +3 -1
- package/apps/shared/mail/zeroAccessSource.ts +27 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +84 -24
- package/apps/shared/search/localIndexBuilder.ts +57 -20
- package/apps/shared/search/localIndexRpcClient.ts +96 -12
- package/apps/shared/search/localIndexSchema.ts +20 -4
- package/apps/shared/search/localIndexTransport.ts +6 -2
- package/apps/shared/search/localIndexWorker.ts +37 -18
- package/apps/shared/search/searchTier2.ts +5 -0
- package/apps/shared/search/zeroAccessIndexSync.ts +679 -0
- package/apps/shared/search/zeroAccessSyncState.ts +70 -0
- package/apps/shared/zeroAccess/ProtectedMark.tsx +19 -0
- package/apps/shared/zeroAccess/useKeysLocked.ts +25 -0
- package/apps/shared/zeroAccess/useOpenedRecord.ts +71 -0
- package/apps/www/_shell.tsx +2 -0
- package/apps/www/calendar/index.tsx +225 -61
- package/apps/www/contacts/index.tsx +67 -11
- package/apps/www/index.tsx +19 -3
- package/apps/www/settings/encryption/index.tsx +21 -1
- package/apps/www/settings/privacy/index.tsx +89 -39
- package/apps/www/tasks/index.tsx +24 -6
- package/dist/apps/escrow/matters/[uid].js +2 -2
- package/dist/apps/shared/calendar/calendarReminders.d.ts +14 -0
- package/dist/apps/shared/calendar/calendarReminders.js +26 -0
- package/dist/apps/shared/calendar/sealedEvents.d.ts +14 -0
- package/dist/apps/shared/calendar/sealedEvents.js +85 -0
- package/dist/apps/shared/calendar/useCalendarReminders.d.ts +2 -1
- package/dist/apps/shared/calendar/useCalendarReminders.js +27 -9
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +3 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +25 -2
- package/dist/apps/shared/components/calendar/EventDetails.js +13 -2
- package/dist/apps/shared/components/calendar/EventEditor.js +115 -18
- package/dist/apps/shared/components/calendar/EventFormParts.js +1 -1
- package/dist/apps/shared/components/calendar/EventModal.d.ts +1 -1
- package/dist/apps/shared/components/calendar/EventModal.js +15 -2
- package/dist/apps/shared/components/calendar/RequestChangeForm.d.ts +3 -0
- package/dist/apps/shared/components/calendar/RequestChangeForm.js +1 -1
- package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -0
- package/dist/apps/shared/components/contacts/ContactCard.js +12 -1
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +2 -1
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +103 -9
- package/dist/apps/shared/components/contacts/ContactForm.d.ts +4 -1
- package/dist/apps/shared/components/contacts/ContactForm.js +37 -7
- package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +3 -1
- package/dist/apps/shared/components/contacts/KeyChangeReview.js +3 -2
- package/dist/apps/shared/components/contacts/contactCardData.js +8 -4
- package/dist/apps/shared/components/contacts/contactKeys.d.ts +1 -1
- package/dist/apps/shared/components/contacts/contactKeys.js +3 -3
- package/dist/apps/shared/components/layout/AppShell.d.ts +18 -1
- package/dist/apps/shared/components/layout/AppShell.js +17 -6
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +27 -2
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.d.ts +17 -0
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.js +98 -0
- package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +3 -0
- package/dist/apps/shared/components/layout/UnlockPromptProvider.js +32 -0
- package/dist/apps/shared/components/mail/ConversationList.js +1 -1
- package/dist/apps/shared/components/mail/InviteCard.js +11 -1
- package/dist/apps/shared/components/mail/MessageDetailPane.js +46 -21
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +2 -2
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +3 -2
- package/dist/apps/shared/components/mail/compose/draftResume.d.ts +2 -0
- package/dist/apps/shared/components/mail/compose/draftResume.js +28 -3
- package/dist/apps/shared/components/mail/compose/protectedContactSuggestions.d.ts +14 -0
- package/dist/apps/shared/components/mail/compose/protectedContactSuggestions.js +96 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +1 -1
- package/dist/apps/shared/components/mail/compose/quotedBody.js +4 -3
- package/dist/apps/shared/components/mail/invite/inviteStore.js +16 -1
- package/dist/apps/shared/components/mail/layout/MailShell.js +2 -1
- package/dist/apps/shared/components/mail/pinnedSigners.d.ts +6 -0
- package/dist/apps/shared/components/mail/pinnedSigners.js +10 -1
- package/dist/apps/shared/components/mail/reading/conversationPrint.d.ts +2 -2
- package/dist/apps/shared/components/mail/reading/conversationPrint.js +3 -3
- package/dist/apps/shared/components/mail/reading/useMessageActions.js +4 -1
- package/dist/apps/shared/components/settings/KeyExportSetting.d.ts +13 -0
- package/dist/apps/shared/components/settings/KeyExportSetting.js +90 -0
- package/dist/apps/shared/components/settings/OpenPgpKeySetting.d.ts +13 -0
- package/dist/apps/shared/components/settings/OpenPgpKeySetting.js +143 -0
- package/dist/apps/shared/components/settings/PolicyLockedNotice.d.ts +11 -0
- package/dist/apps/shared/components/settings/PolicyLockedNotice.js +9 -0
- package/dist/apps/shared/components/settings/ZeroAccessSetting.d.ts +16 -0
- package/dist/apps/shared/components/settings/ZeroAccessSetting.js +80 -0
- package/dist/apps/shared/components/settings/ZeroAccessTypes.d.ts +29 -0
- package/dist/apps/shared/components/settings/ZeroAccessTypes.js +107 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +3 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +3 -2
- package/dist/apps/shared/mail/decryptedMessages.d.ts +1 -1
- package/dist/apps/shared/mail/decryptedMessages.js +11 -3
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +37 -4
- package/dist/apps/shared/mail/outbox/sendJob.js +287 -42
- package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +12 -0
- package/dist/apps/shared/mail/outbox/sendOutcomes.js +76 -20
- package/dist/apps/shared/mail/reportNotices.js +3 -1
- package/dist/apps/shared/mail/zeroAccessSource.d.ts +8 -0
- package/dist/apps/shared/mail/zeroAccessSource.js +26 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +2 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +74 -24
- package/dist/apps/shared/search/localIndexBuilder.d.ts +9 -1
- package/dist/apps/shared/search/localIndexBuilder.js +50 -17
- package/dist/apps/shared/search/localIndexRpcClient.d.ts +5 -39
- package/dist/apps/shared/search/localIndexRpcClient.js +129 -11
- package/dist/apps/shared/search/localIndexSchema.d.ts +14 -2
- package/dist/apps/shared/search/localIndexSchema.js +18 -4
- package/dist/apps/shared/search/localIndexTransport.d.ts +6 -2
- package/dist/apps/shared/search/localIndexWorker.d.ts +11 -0
- package/dist/apps/shared/search/localIndexWorker.js +22 -15
- package/dist/apps/shared/search/searchTier2.js +5 -0
- package/dist/apps/shared/search/zeroAccessIndexSync.d.ts +102 -0
- package/dist/apps/shared/search/zeroAccessIndexSync.js +544 -0
- package/dist/apps/shared/search/zeroAccessSyncState.d.ts +20 -0
- package/dist/apps/shared/search/zeroAccessSyncState.js +65 -0
- package/dist/apps/shared/zeroAccess/ProtectedMark.d.ts +7 -0
- package/dist/apps/shared/zeroAccess/ProtectedMark.js +10 -0
- package/dist/apps/shared/zeroAccess/useKeysLocked.d.ts +6 -0
- package/dist/apps/shared/zeroAccess/useKeysLocked.js +20 -0
- package/dist/apps/shared/zeroAccess/useOpenedRecord.d.ts +18 -0
- package/dist/apps/shared/zeroAccess/useOpenedRecord.js +66 -0
- package/dist/apps/www/_shell.d.ts +1 -1
- package/dist/apps/www/_shell.js +2 -2
- package/dist/apps/www/calendar/index.js +88 -20
- package/dist/apps/www/contacts/index.js +64 -11
- package/dist/apps/www/index.js +19 -3
- package/dist/apps/www/settings/encryption/index.js +16 -4
- package/dist/apps/www/settings/privacy/index.js +47 -6
- package/dist/apps/www/tasks/index.js +21 -7
- package/dist/lib/admin/domainsApi.d.ts +1 -1
- package/dist/lib/admin/escrowAccessRequestsApi.d.ts +6 -0
- package/dist/lib/calendar/calendarApi.d.ts +52 -6
- package/dist/lib/calendar/calendarApi.js +110 -17
- package/dist/lib/calendar/calendarMutations.d.ts +20 -7
- package/dist/lib/calendar/calendarMutations.js +73 -14
- package/dist/lib/calendar/inviteApi.d.ts +17 -3
- package/dist/lib/calendar/inviteApi.js +86 -7
- package/dist/lib/calendar/protectedEvents.d.ts +31 -0
- package/dist/lib/calendar/protectedEvents.js +113 -0
- package/dist/lib/calendar/recurrence.d.ts +2 -0
- package/dist/lib/calendar/recurrence.js +16 -0
- package/dist/lib/calendar/sealedInvites.d.ts +9 -0
- package/dist/lib/calendar/sealedInvites.js +65 -0
- package/dist/lib/contacts/contactsApi.d.ts +29 -3
- package/dist/lib/contacts/contactsApi.js +23 -7
- package/dist/lib/contacts/useContactPhotoSrc.d.ts +5 -2
- package/dist/lib/contacts/useContactPhotoSrc.js +91 -11
- package/dist/lib/crypto/composeSecurity.d.ts +11 -3
- package/dist/lib/crypto/composeSecurity.js +13 -5
- package/dist/lib/crypto/keyExport.d.ts +54 -0
- package/dist/lib/crypto/keyExport.js +161 -0
- package/dist/lib/crypto/keyPairMatch.d.ts +9 -0
- package/dist/lib/crypto/keyPairMatch.js +65 -0
- package/dist/lib/crypto/keySession.d.ts +64 -3
- package/dist/lib/crypto/keySession.js +179 -12
- package/dist/lib/crypto/keyvaultApi.d.ts +129 -8
- package/dist/lib/crypto/keyvaultApi.js +45 -3
- package/dist/lib/crypto/meetingKeys.d.ts +25 -0
- package/dist/lib/crypto/meetingKeys.js +87 -0
- package/dist/lib/crypto/messageSecurity.d.ts +56 -3
- package/dist/lib/crypto/messageSecurity.js +201 -13
- package/dist/lib/crypto/mime.d.ts +5 -0
- package/dist/lib/crypto/mime.js +25 -0
- package/dist/lib/crypto/openPgpKey.d.ts +39 -0
- package/dist/lib/crypto/openPgpKey.js +89 -0
- package/dist/lib/crypto/openpgp.d.ts +47 -0
- package/dist/lib/crypto/openpgp.js +88 -0
- package/dist/lib/crypto/pgpMessage.d.ts +51 -0
- package/dist/lib/crypto/pgpMessage.js +271 -0
- package/dist/lib/crypto/sharedMailboxKeys.d.ts +62 -0
- package/dist/lib/crypto/sharedMailboxKeys.js +337 -0
- package/dist/lib/crypto/smimeMessage.d.ts +21 -0
- package/dist/lib/crypto/smimeMessage.js +11 -7
- package/dist/lib/crypto/zeroAccessKey.d.ts +32 -0
- package/dist/lib/crypto/zeroAccessKey.js +71 -0
- package/dist/lib/mail/conversationsApi.d.ts +6 -0
- package/dist/lib/mail/mailApi.d.ts +63 -2
- package/dist/lib/mail/mailApi.js +5 -1
- package/dist/lib/mail/mailboxImportApi.d.ts +10 -0
- package/dist/lib/mail/mailboxImportApi.js +15 -0
- package/dist/lib/search/zeroAccessIndexApi.d.ts +85 -0
- package/dist/lib/search/zeroAccessIndexApi.js +35 -0
- package/dist/lib/tasks/tasksApi.d.ts +11 -1
- package/dist/lib/tasks/tasksApi.js +10 -4
- package/dist/lib/util/clientPlatform.d.ts +14 -0
- package/dist/lib/util/clientPlatform.js +26 -0
- package/dist/lib/videoconf/protectedMeetings.d.ts +74 -0
- package/dist/lib/videoconf/protectedMeetings.js +181 -0
- package/dist/lib/videoconf/videoMeetingsApi.d.ts +25 -4
- package/dist/lib/videoconf/videoMeetingsApi.js +1 -1
- package/dist/lib/zeroAccess/sealedMoves.d.ts +41 -0
- package/dist/lib/zeroAccess/sealedMoves.js +63 -0
- package/dist/lib/zeroAccess/sealedRecords.d.ts +29 -0
- package/dist/lib/zeroAccess/sealedRecords.js +136 -0
- package/lib/admin/domainsApi.ts +1 -1
- package/lib/admin/escrowAccessRequestsApi.ts +6 -0
- package/lib/calendar/calendarApi.ts +143 -17
- package/lib/calendar/calendarMutations.ts +79 -12
- package/lib/calendar/inviteApi.ts +101 -8
- package/lib/calendar/protectedEvents.ts +143 -0
- package/lib/calendar/recurrence.ts +17 -0
- package/lib/calendar/sealedInvites.ts +73 -0
- package/lib/contacts/contactsApi.ts +43 -17
- package/lib/contacts/useContactPhotoSrc.ts +103 -13
- package/lib/crypto/composeSecurity.ts +20 -5
- package/lib/crypto/keyExport.ts +229 -0
- package/lib/crypto/keyPairMatch.ts +69 -0
- package/lib/crypto/keySession.ts +216 -12
- package/lib/crypto/keyvaultApi.ts +174 -10
- package/lib/crypto/meetingKeys.ts +118 -0
- package/lib/crypto/messageSecurity.ts +284 -16
- package/lib/crypto/mime.ts +26 -0
- package/lib/crypto/openPgpKey.ts +118 -0
- package/lib/crypto/openpgp.ts +115 -0
- package/lib/crypto/pgpMessage.ts +336 -0
- package/lib/crypto/sharedMailboxKeys.ts +402 -0
- package/lib/crypto/smimeMessage.ts +11 -7
- package/lib/crypto/zeroAccessKey.ts +102 -0
- package/lib/mail/conversationsApi.ts +3 -0
- package/lib/mail/mailApi.ts +60 -3
- package/lib/mail/mailboxImportApi.ts +24 -0
- package/lib/search/zeroAccessIndexApi.ts +123 -0
- package/lib/tasks/tasksApi.ts +19 -5
- package/lib/util/clientPlatform.ts +50 -0
- package/lib/videoconf/protectedMeetings.ts +221 -0
- package/lib/videoconf/videoMeetingsApi.ts +15 -4
- package/lib/zeroAccess/sealedMoves.ts +79 -0
- package/lib/zeroAccess/sealedRecords.ts +153 -0
- package/package.json +14 -5
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
createContact,
|
|
12
12
|
deleteContact,
|
|
13
13
|
getContact,
|
|
14
|
+
CONTACT_LIST_FIELDS,
|
|
15
|
+
CONTACT_MATCH_FIELDS,
|
|
14
16
|
listContacts,
|
|
15
17
|
ensureSuggestedContacts,
|
|
16
18
|
listContactsInFolders,
|
|
@@ -44,6 +46,8 @@ import { useShortcut } from "../../shared/keyboard/useShortcut.js";
|
|
|
44
46
|
import { notifyApiError } from "../../shared/notifications/apiErrors.js";
|
|
45
47
|
import { notify } from "../../shared/notifications/store.js";
|
|
46
48
|
import { changedItemOf, useFolderLiveUpdates } from "../../shared/live/useFolderLiveUpdates.js";
|
|
49
|
+
import { isOpenedRecord, isProtectedRecord, openSealedRecord, openSealedRecords } from "../../../lib/zeroAccess/sealedRecords.js";
|
|
50
|
+
import { useKeysLocked } from "../../shared/zeroAccess/useKeysLocked.js";
|
|
47
51
|
|
|
48
52
|
/** The published model class name of a contact on either database (`ContactMongo`, `ContactSQL`) - not a `ContactList`'s. */
|
|
49
53
|
const CONTACT_MODEL = /^Contact(Mongo|SQL)$/;
|
|
@@ -142,6 +146,7 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
142
146
|
void loadSuggested(suggestedFolderUid);
|
|
143
147
|
}
|
|
144
148
|
return listAllPages((page) => listContacts(folderUid, { limit: LIST_PAGE_SIZE, page }, client))
|
|
149
|
+
.then(async (result) => ({ ...result, items: await openSealedRecords(result.items, CONTACT_LIST_FIELDS) }))
|
|
145
150
|
.then((result) => {
|
|
146
151
|
setContacts(result.items);
|
|
147
152
|
setTruncated(result.truncated);
|
|
@@ -154,10 +159,25 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
154
159
|
void reload();
|
|
155
160
|
}, [folderUid]);
|
|
156
161
|
|
|
162
|
+
// The lists hold protected contacts opened with the mailbox's keys: its lock takes them off screen, read again with their placeholders (the
|
|
163
|
+
// contacts, the suggested ones and the Deleted view). They leave the lists first, so a reload that fails can't leave them shown. An open
|
|
164
|
+
// contact's pane and form drop what they opened themselves (`useOpenedRecord`).
|
|
165
|
+
const [lockCount, setLockCount] = useState(0);
|
|
166
|
+
useKeysLocked((locked) => {
|
|
167
|
+
if (locked === mailboxUid) {
|
|
168
|
+
const readable = (list: Contact[]) => list.filter((contact) => !isProtectedRecord(contact));
|
|
169
|
+
setContacts(readable);
|
|
170
|
+
setSuggestedContacts(readable);
|
|
171
|
+
setDeletedContacts(readable);
|
|
172
|
+
void reload();
|
|
173
|
+
setLockCount((count) => count + 1);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
157
177
|
/** Reads the Suggested Contacts folder's contacts. A failure leaves the list as it was. */
|
|
158
178
|
function loadSuggested(suggestedUid: string): Promise<void> {
|
|
159
179
|
return listAllPages((page) => listContacts(suggestedUid, { limit: LIST_PAGE_SIZE, page }, client))
|
|
160
|
-
.then((result) => setSuggestedContacts(result.items))
|
|
180
|
+
.then(async (result) => setSuggestedContacts(await openSealedRecords(result.items, CONTACT_LIST_FIELDS)))
|
|
161
181
|
.catch(() => undefined);
|
|
162
182
|
}
|
|
163
183
|
|
|
@@ -195,7 +215,9 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
195
215
|
remove(change.uid);
|
|
196
216
|
return;
|
|
197
217
|
}
|
|
198
|
-
void getContact(change.uid, client)
|
|
218
|
+
void getContact(change.uid, client)
|
|
219
|
+
.then((fresh) => openSealedRecord(fresh, CONTACT_LIST_FIELDS))
|
|
220
|
+
.then(
|
|
199
221
|
(fresh) => {
|
|
200
222
|
if (fresh.deleted === true) {
|
|
201
223
|
remove(fresh.uid);
|
|
@@ -256,6 +278,7 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
256
278
|
setDeletedLoading(true);
|
|
257
279
|
setDeletedError(null);
|
|
258
280
|
listAllPages((page) => listDeletedContacts(folderUid, { limit: LIST_PAGE_SIZE, page }, client))
|
|
281
|
+
.then(async (result) => ({ ...result, items: await openSealedRecords(result.items, CONTACT_LIST_FIELDS) }))
|
|
259
282
|
.then((result) => {
|
|
260
283
|
// A server that dropped the `deleted` filter (no delete+update right on this folder) answers with live
|
|
261
284
|
// contacts, which must never be listed as deleted.
|
|
@@ -271,8 +294,8 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
271
294
|
.catch((err) => setDeletedError(err instanceof ApiRequestError ? err.message : "Could not load deleted contacts."))
|
|
272
295
|
.finally(() => setDeletedLoading(false));
|
|
273
296
|
// Re-fetches every time the Deleted view is (re-)selected, matching this page's own `reload()`
|
|
274
|
-
// convention elsewhere rather than caching a possibly-stale list across visits.
|
|
275
|
-
}, [view, folderUid]);
|
|
297
|
+
// convention elsewhere rather than caching a possibly-stale list across visits - and when the mailbox locks.
|
|
298
|
+
}, [view, folderUid, lockCount]);
|
|
276
299
|
|
|
277
300
|
const viewFiltered = useMemo(() => {
|
|
278
301
|
switch (view.type) {
|
|
@@ -467,9 +490,17 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
467
490
|
if (!category?.trim()) {
|
|
468
491
|
return;
|
|
469
492
|
}
|
|
470
|
-
|
|
471
|
-
|
|
493
|
+
let locked = 0;
|
|
494
|
+
for (const listed of checkedContacts) {
|
|
472
495
|
try {
|
|
496
|
+
// A protected one's categories are added to as opened: one that won't open here (the mailbox is locked) shows a placeholder
|
|
497
|
+
// (none), and saving that plus the new one would replace its real categories for good. It is left as it is.
|
|
498
|
+
const contact = await openSealedRecord(listed);
|
|
499
|
+
if (isProtectedRecord(contact) && !isOpenedRecord(contact)) {
|
|
500
|
+
locked++;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
const categories = Array.from(new Set([...(contact.categories ?? []), category.trim()]));
|
|
473
504
|
// Only the changed field - sending the whole fetched contact back includes server-managed
|
|
474
505
|
// fields restapi rejects, and would overwrite any concurrent edit to the other fields.
|
|
475
506
|
await updateContact({ uid: contact.uid, version: contact.version, categories }, client);
|
|
@@ -477,6 +508,13 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
477
508
|
notifyApiError(err, "Couldn't update some of the contacts");
|
|
478
509
|
}
|
|
479
510
|
}
|
|
511
|
+
if (locked > 0) {
|
|
512
|
+
notify({
|
|
513
|
+
kind: "warning",
|
|
514
|
+
title: "Some contacts weren't categorized",
|
|
515
|
+
message: `${locked === 1 ? "A contact is" : `${locked} contacts are`} protected and can't be changed while your mailbox is locked. Unlock it, then add the category again.`,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
480
518
|
clearPinnedSignerCache();
|
|
481
519
|
await reload();
|
|
482
520
|
});
|
|
@@ -484,8 +522,17 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
484
522
|
// A suggested contact becomes one of the mailbox's own: a copy in its contacts folder, and the suggestion removed (the person is not suggested again).
|
|
485
523
|
const handleAddToContacts = singleFlight(async () => {
|
|
486
524
|
// In the All contacts view the selection can hold contacts that already are the mailbox's own: only the suggested ones are added.
|
|
487
|
-
|
|
525
|
+
let locked = 0;
|
|
526
|
+
for (const listed of checkedContacts.filter((checked) => checked.folderUid === suggestedFolderUid)) {
|
|
488
527
|
try {
|
|
528
|
+
// Copied in full: the list opened only what it shows.
|
|
529
|
+
const contact = await openSealedRecord(listed);
|
|
530
|
+
// A protected one that won't open here (the mailbox is locked) holds only placeholders: copying them would replace its real
|
|
531
|
+
// name for good once the original is deleted. It is left as it is.
|
|
532
|
+
if (isProtectedRecord(contact) && !isOpenedRecord(contact)) {
|
|
533
|
+
locked++;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
489
536
|
await createContact(
|
|
490
537
|
{
|
|
491
538
|
mailboxUid: contact.mailboxUid,
|
|
@@ -508,14 +555,23 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
508
555
|
notifyApiError(err, "Couldn't add some of the contacts");
|
|
509
556
|
}
|
|
510
557
|
}
|
|
558
|
+
if (locked > 0) {
|
|
559
|
+
notify({
|
|
560
|
+
kind: "warning",
|
|
561
|
+
title: "Some contacts weren't added",
|
|
562
|
+
message: `${locked === 1 ? "A suggested contact is" : `${locked} suggested contacts are`} protected and can't be opened while your mailbox is locked. Unlock it, then add ${locked === 1 ? "it" : "them"} again.`,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
511
565
|
setCheckedUids(new Set());
|
|
512
566
|
await reload();
|
|
513
567
|
});
|
|
514
568
|
|
|
515
|
-
function handleExportVCard() {
|
|
569
|
+
async function handleExportVCard() {
|
|
570
|
+
// Exported in full: the list opened only what it shows.
|
|
571
|
+
const contacts = await openSealedRecords(checkedContacts);
|
|
516
572
|
downloadTextFile(
|
|
517
|
-
|
|
518
|
-
|
|
573
|
+
contacts.length === 1 ? `${contacts[0].displayName}.vcf` : "contacts.vcf",
|
|
574
|
+
contacts.length === 1 ? contactToVCard(contacts[0]) : contactsToVCardFile(contacts),
|
|
519
575
|
);
|
|
520
576
|
}
|
|
521
577
|
|
|
@@ -543,7 +599,7 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
543
599
|
// earlier card's in the file - is left alone. The folder is read in full for that, not just the page of it the list holds.
|
|
544
600
|
let fresh: ParsedVCardContact[];
|
|
545
601
|
try {
|
|
546
|
-
fresh = withoutDuplicates(parsed, await listContactsInFolders([folderUid], client)).fresh;
|
|
602
|
+
fresh = withoutDuplicates(parsed, await openSealedRecords(await listContactsInFolders([folderUid], client), CONTACT_MATCH_FIELDS)).fresh;
|
|
547
603
|
} catch (err) {
|
|
548
604
|
notifyApiError(err, "Couldn't check the file against your contacts");
|
|
549
605
|
return;
|
package/apps/www/index.tsx
CHANGED
|
@@ -159,12 +159,28 @@ async function decryptEncryptedRows(messages: Message[], unlocked: UnlockedKeys)
|
|
|
159
159
|
encrypted,
|
|
160
160
|
DECRYPT_CONCURRENCY,
|
|
161
161
|
async (message): Promise<[string, DecryptedRow] | null> => {
|
|
162
|
+
// A lock while a row was opened has already cleared the shared store: what it opened must not go back in (`decryptForDisplay()`
|
|
163
|
+
// asks the same). The caller's lock generation keeps it off this page.
|
|
164
|
+
const stillUnlocked = (): boolean => getUnlockedKeys(message.mailboxUid) === unlocked;
|
|
162
165
|
try {
|
|
163
|
-
const rawMime = await getMessageRawContent(message.uid);
|
|
164
166
|
// Loaded here, on first use: the S/MIME code (PKI.js, ASN.1, X.509) is over half a megabyte and an inbox with
|
|
165
167
|
// no encrypted mail in it never needs it.
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
+
const securityApi = await (securityModule ??= import("../../lib/crypto/messageSecurity.js"));
|
|
169
|
+
// A message sealed at rest carries its subject and preview sealed on the row: no download needed.
|
|
170
|
+
const summary = message.zeroAccess ? await securityApi.openZeroAccessSummary(message.zeroAccess.sealedSummary, unlocked.zeroAccessKeys) : undefined;
|
|
171
|
+
if (summary) {
|
|
172
|
+
const preview = summary.preview.slice(0, LIST_PREVIEW_MAX_LENGTH);
|
|
173
|
+
if (stillUnlocked()) {
|
|
174
|
+
rememberDecrypted(message.uid, message.mailboxUid, { subject: summary.subject, preview });
|
|
175
|
+
}
|
|
176
|
+
return [message.uid, { subject: summary.subject, preview }];
|
|
177
|
+
}
|
|
178
|
+
const rawMime = await getMessageRawContent(message.uid);
|
|
179
|
+
const security = await securityApi.evaluateMessageSecurity(rawMime, unlocked);
|
|
180
|
+
// Nor is a lock a failure to decrypt.
|
|
181
|
+
if (!stillUnlocked()) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
168
184
|
if (!security.subject && !security.html) {
|
|
169
185
|
markDecryptFailed(message.uid);
|
|
170
186
|
return null;
|
|
@@ -17,14 +17,17 @@ import {
|
|
|
17
17
|
getCurrentSignEnrollment,
|
|
18
18
|
getEscrowInfo,
|
|
19
19
|
getKeyVault,
|
|
20
|
+
isOpenPgpKey,
|
|
20
21
|
rekey,
|
|
21
22
|
removeMasterKeyWrap,
|
|
22
23
|
startSignEnrollment,
|
|
23
24
|
} from "../../../../lib/crypto/keyvaultApi.js";
|
|
24
25
|
import {
|
|
25
26
|
ENCRYPTION_PRIVATE_KEY_AAD_PURPOSE,
|
|
27
|
+
OPENPGP_PRIVATE_KEY_AAD_PURPOSE,
|
|
26
28
|
SIGNING_PRIVATE_KEY_AAD_PURPOSE,
|
|
27
29
|
UnlockedKeys,
|
|
30
|
+
ZERO_ACCESS_PRIVATE_KEY_AAD_PURPOSE,
|
|
28
31
|
destroyUnlockedKeys,
|
|
29
32
|
getUnlockedKeys,
|
|
30
33
|
unlockWithPassword,
|
|
@@ -51,6 +54,9 @@ import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
|
|
|
51
54
|
import Alert from "../../../../lib/components/feedback/Alert.js";
|
|
52
55
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
53
56
|
import Modal from "../../../../lib/components/overlays/Modal.js";
|
|
57
|
+
import ZeroAccessSetting from "../../../shared/components/settings/ZeroAccessSetting.js";
|
|
58
|
+
import KeyExportSetting from "../../../shared/components/settings/KeyExportSetting.js";
|
|
59
|
+
import OpenPgpKeySetting from "../../../shared/components/settings/OpenPgpKeySetting.js";
|
|
54
60
|
import SigningCertificateCard, { SigningCertificateMode } from "../../../shared/components/settings/SigningCertificateCard.js";
|
|
55
61
|
import { readStoredSignEnrollment, storeSignEnrollment } from "../../../shared/signing/enrollmentStorage.js";
|
|
56
62
|
import {
|
|
@@ -101,7 +107,14 @@ class StaleSessionKeysError extends Error {
|
|
|
101
107
|
const STALE_KEYS_MESSAGE =
|
|
102
108
|
"Your encryption keys were changed on another device (for example, rotated), so the copy unlocked in this session no longer works. Nothing was changed. Unlock again with your current password, then try again.";
|
|
103
109
|
|
|
110
|
+
/** The AAD a vault key is sealed with - by what it is: an OpenPGP secret key, a zero-access key, a signing key or an encryption key. */
|
|
104
111
|
function wrappedKeyAad(mailboxUid: string, entry: WrappedPrivateKey): Uint8Array {
|
|
112
|
+
if (isOpenPgpKey(entry)) {
|
|
113
|
+
return buildAad(mailboxUid, OPENPGP_PRIVATE_KEY_AAD_PURPOSE);
|
|
114
|
+
}
|
|
115
|
+
if (entry.useType === "zero-access") {
|
|
116
|
+
return buildAad(mailboxUid, ZERO_ACCESS_PRIVATE_KEY_AAD_PURPOSE);
|
|
117
|
+
}
|
|
105
118
|
return buildAad(mailboxUid, entry.useType === "sign" ? SIGNING_PRIVATE_KEY_AAD_PURPOSE : ENCRYPTION_PRIVATE_KEY_AAD_PURPOSE);
|
|
106
119
|
}
|
|
107
120
|
|
|
@@ -167,6 +180,8 @@ async function rewrapVaultPrivateKeys(
|
|
|
167
180
|
algorithm: "AES-256-GCM",
|
|
168
181
|
fingerprint: entry.fingerprint,
|
|
169
182
|
useType: entry.useType,
|
|
183
|
+
// An OpenPGP secret key stays one: without its type it would read back as an S/MIME key.
|
|
184
|
+
...(isOpenPgpKey(entry) ? { type: entry.type } : {}),
|
|
170
185
|
});
|
|
171
186
|
}
|
|
172
187
|
return { mk, wrappedKeys: rewrapped };
|
|
@@ -227,7 +242,8 @@ function EncryptionGate({ userUid, impersonating }: { userUid?: string; imperson
|
|
|
227
242
|
mailboxKeys={mailbox.keys}
|
|
228
243
|
canProvision={isOwner}
|
|
229
244
|
>
|
|
230
|
-
|
|
245
|
+
{/* Mounted anew for each mailbox: switching mailbox keeps the page, and its settings seed their state from the mailbox. */}
|
|
246
|
+
<EncryptionContent key={mailboxUid} canManageKeys={isOwner} />
|
|
231
247
|
</KeyEnrollmentGate>
|
|
232
248
|
);
|
|
233
249
|
}
|
|
@@ -975,6 +991,10 @@ function EncryptionContent({ canManageKeys }: { canManageKeys: boolean }) {
|
|
|
975
991
|
)}
|
|
976
992
|
</div>
|
|
977
993
|
|
|
994
|
+
<ZeroAccessSetting mailbox={mailbox} canManage={canManageKeys} />
|
|
995
|
+
<KeyExportSetting mailbox={mailbox} canManage={canManageKeys} />
|
|
996
|
+
<OpenPgpKeySetting mailbox={mailbox} canManage={canManageKeys} />
|
|
997
|
+
|
|
978
998
|
<div>
|
|
979
999
|
<h2 className="text-sm font-semibold mb-2">Digital signatures</h2>
|
|
980
1000
|
{activeSigningKey && signingStatus !== "pending" && (
|
|
@@ -15,21 +15,25 @@ import {
|
|
|
15
15
|
import {
|
|
16
16
|
MailboxImportFormat,
|
|
17
17
|
MailboxImportRequest,
|
|
18
|
+
ZERO_ACCESS_IMPORT_MESSAGE,
|
|
18
19
|
listImportRequests,
|
|
20
|
+
mailIsZeroAccess,
|
|
19
21
|
uploadMailboxImport,
|
|
20
22
|
} from "../../../../lib/mail/mailboxImportApi.js";
|
|
23
|
+
import { getEncryptionPolicy } from "../../../../lib/crypto/keyvaultApi.js";
|
|
21
24
|
import {
|
|
22
25
|
DataSubjectErasureRequest,
|
|
23
26
|
createErasureRequest,
|
|
24
27
|
listErasureRequests,
|
|
25
28
|
} from "../../../../lib/mail/erasureRequestApi.js";
|
|
26
|
-
import { Folder, listFolders } from "../../../../lib/mail/mailApi.js";
|
|
29
|
+
import { Folder, Mailbox, listFolders } from "../../../../lib/mail/mailApi.js";
|
|
27
30
|
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
28
31
|
import SettingsShell, { SettingsShellProps, useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
29
32
|
import Alert from "../../../../lib/components/feedback/Alert.js";
|
|
30
33
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
31
34
|
import Modal from "../../../../lib/components/overlays/Modal.js";
|
|
32
35
|
import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
|
|
36
|
+
import { INVALID_REQUEST_CODE } from "../../../../lib/zeroAccess/sealedMoves.js";
|
|
33
37
|
import GravatarSetting from "../../../shared/components/settings/GravatarSetting.js";
|
|
34
38
|
|
|
35
39
|
const NON_MAIL_FOLDER_TYPES = new Set(["calendar", "contacts", "tasks", "notes"]);
|
|
@@ -89,7 +93,7 @@ function PrivacyContent({ userUid }: { userUid?: string }) {
|
|
|
89
93
|
{selectedIsOwn ? (
|
|
90
94
|
<>
|
|
91
95
|
<ExportSection mailboxUid={mailboxUid} mailboxLabel={`${selected.displayName} (${selected.primarySmtpAddress})`} />
|
|
92
|
-
<ImportSection mailboxUid={mailboxUid} />
|
|
96
|
+
<ImportSection mailboxUid={mailboxUid} mailbox={selected} />
|
|
93
97
|
<ErasureSection mailboxLabel={`${selected.displayName} (${selected.primarySmtpAddress})`} />
|
|
94
98
|
</>
|
|
95
99
|
) : (
|
|
@@ -174,7 +178,8 @@ function ExportSection({ mailboxUid, mailboxLabel }: { mailboxUid?: string; mail
|
|
|
174
178
|
<p className="text-xs text-text-muted mb-3">
|
|
175
179
|
Requests a copy of <strong>{mailboxLabel}</strong>’s messages, contacts, calendar, tasks, and notes. This runs
|
|
176
180
|
in the background and can take a few minutes — check back here for a download link once it’s
|
|
177
|
-
ready.
|
|
181
|
+
ready. Mail stored with zero-access encryption is exported encrypted: open it in a mail app that has your
|
|
182
|
+
exported keys (Settings › Encryption).
|
|
178
183
|
</p>
|
|
179
184
|
|
|
180
185
|
<form onSubmit={handleCreate} className="flex items-end gap-3 mb-5">
|
|
@@ -237,8 +242,36 @@ function ExportSection({ mailboxUid, mailboxLabel }: { mailboxUid?: string; mail
|
|
|
237
242
|
);
|
|
238
243
|
}
|
|
239
244
|
|
|
240
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Uploads a mail archive for restapi's server-side import. Not offered for a mailbox whose mail is zero-access (`mailIsZeroAccess()`):
|
|
247
|
+
* restapi refuses it (409), since the archive would wait readable in its blob store until the import ran, and the reason is shown
|
|
248
|
+
* instead. The server's zero-access refusal given anyway (zero-access turned on since the page loaded) is shown in its place too; its other
|
|
249
|
+
* 409, an import already in progress, is not.
|
|
250
|
+
*/
|
|
251
|
+
function ImportSection({ mailboxUid, mailbox }: { mailboxUid?: string; mailbox: Mailbox }) {
|
|
241
252
|
const client = useApiClient();
|
|
253
|
+
// Only when the mailbox's own fields say its mail is zero-access is the administrator's policy asked: `undefined` until it answers
|
|
254
|
+
// (counted as allowed), and a failed read counts as allowed too, so the upload is never offered only to be refused.
|
|
255
|
+
const zeroAccessCandidate = mailIsZeroAccess(mailbox, undefined);
|
|
256
|
+
const [allowZeroAccess, setAllowZeroAccess] = useState<boolean | undefined>(undefined);
|
|
257
|
+
const [refused, setRefused] = useState<string | null>(null);
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
if (!zeroAccessCandidate) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
let cancelled = false;
|
|
263
|
+
void getEncryptionPolicy(client)
|
|
264
|
+
.then((policy) => {
|
|
265
|
+
if (!cancelled) {
|
|
266
|
+
setAllowZeroAccess(policy.allowZeroAccess);
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
.catch(() => undefined);
|
|
270
|
+
return () => {
|
|
271
|
+
cancelled = true;
|
|
272
|
+
};
|
|
273
|
+
}, [zeroAccessCandidate, client]);
|
|
274
|
+
const zeroAccess = mailIsZeroAccess(mailbox, allowZeroAccess);
|
|
242
275
|
const [folders, setFolders] = useState<Folder[]>([]);
|
|
243
276
|
const [targetFolderUid, setTargetFolderUid] = useState("");
|
|
244
277
|
const [foldersError, setFoldersError] = useState<string | null>(null);
|
|
@@ -293,7 +326,18 @@ function ImportSection({ mailboxUid }: { mailboxUid?: string }) {
|
|
|
293
326
|
setUploading(true);
|
|
294
327
|
uploadMailboxImport(file, { format: importFormatFromFilename(file.name), targetFolderUid, mailboxUid: mailboxUid! }, client)
|
|
295
328
|
.then(() => loadRequests())
|
|
296
|
-
.catch((err) =>
|
|
329
|
+
.catch((err) => {
|
|
330
|
+
if (err instanceof ApiRequestError && err.status === 409 && err.code === INVALID_REQUEST_CODE) {
|
|
331
|
+
// The server refuses an import into this mailbox (its mail is zero-access): say why, in place of the upload.
|
|
332
|
+
setRefused(err.message);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// Any other refusal - an import already in progress is a 409 too - leaves the upload offered, and shows that import.
|
|
336
|
+
notifyApiError(err, "Couldn't upload the file");
|
|
337
|
+
if (err instanceof ApiRequestError && err.status === 409) {
|
|
338
|
+
void loadRequests();
|
|
339
|
+
}
|
|
340
|
+
})
|
|
297
341
|
.finally(() => setUploading(false));
|
|
298
342
|
}
|
|
299
343
|
|
|
@@ -305,43 +349,49 @@ function ImportSection({ mailboxUid }: { mailboxUid?: string }) {
|
|
|
305
349
|
choose below. This runs in the background — check back here for its progress.
|
|
306
350
|
</p>
|
|
307
351
|
|
|
308
|
-
{
|
|
309
|
-
<
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
className="
|
|
314
|
-
|
|
315
|
-
|
|
352
|
+
{zeroAccess || refused ? (
|
|
353
|
+
<Alert>{refused ?? ZERO_ACCESS_IMPORT_MESSAGE}</Alert>
|
|
354
|
+
) : (
|
|
355
|
+
<>
|
|
356
|
+
{folders.length > 0 && (
|
|
357
|
+
<label className="flex flex-col gap-1.5 text-sm mb-3 max-w-xs">
|
|
358
|
+
<span className="font-semibold">Destination folder</span>
|
|
359
|
+
<select
|
|
360
|
+
aria-label="Destination folder"
|
|
361
|
+
className="text-sm border border-border rounded-sm py-1.5 px-2 bg-surface"
|
|
362
|
+
value={targetFolderUid}
|
|
363
|
+
onChange={(e) => setTargetFolderUid(e.target.value)}
|
|
364
|
+
>
|
|
365
|
+
{folders.map((folder) => (
|
|
366
|
+
<option key={folder.uid} value={folder.uid}>
|
|
367
|
+
{folder.name}
|
|
368
|
+
</option>
|
|
369
|
+
))}
|
|
370
|
+
</select>
|
|
371
|
+
</label>
|
|
372
|
+
)}
|
|
373
|
+
|
|
374
|
+
<Button
|
|
375
|
+
type="button"
|
|
376
|
+
variant="secondary"
|
|
377
|
+
className="!w-auto mb-3"
|
|
378
|
+
loading={uploading}
|
|
379
|
+
disabled={uploading || !targetFolderUid}
|
|
380
|
+
onClick={() => fileInputRef.current?.click()}
|
|
316
381
|
>
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
382
|
+
Upload Mbox or PST file
|
|
383
|
+
</Button>
|
|
384
|
+
<input
|
|
385
|
+
ref={fileInputRef}
|
|
386
|
+
type="file"
|
|
387
|
+
accept=".mbox,.pst"
|
|
388
|
+
aria-label="Upload mail archive"
|
|
389
|
+
className="hidden"
|
|
390
|
+
onChange={handleFileChange}
|
|
391
|
+
/>
|
|
392
|
+
</>
|
|
324
393
|
)}
|
|
325
394
|
|
|
326
|
-
<Button
|
|
327
|
-
type="button"
|
|
328
|
-
variant="secondary"
|
|
329
|
-
className="!w-auto mb-3"
|
|
330
|
-
loading={uploading}
|
|
331
|
-
disabled={uploading || !targetFolderUid}
|
|
332
|
-
onClick={() => fileInputRef.current?.click()}
|
|
333
|
-
>
|
|
334
|
-
Upload Mbox or PST file
|
|
335
|
-
</Button>
|
|
336
|
-
<input
|
|
337
|
-
ref={fileInputRef}
|
|
338
|
-
type="file"
|
|
339
|
-
accept=".mbox,.pst"
|
|
340
|
-
aria-label="Upload mail archive"
|
|
341
|
-
className="hidden"
|
|
342
|
-
onChange={handleFileChange}
|
|
343
|
-
/>
|
|
344
|
-
|
|
345
395
|
{foldersError && <Alert>{foldersError}</Alert>}
|
|
346
396
|
{loadError && <Alert>{loadError}</Alert>}
|
|
347
397
|
|
package/apps/www/tasks/index.tsx
CHANGED
|
@@ -36,6 +36,9 @@ import { useShortcut } from "../../shared/keyboard/useShortcut.js";
|
|
|
36
36
|
import { useShortcutProps } from "../../shared/keyboard/useShortcutProps.js";
|
|
37
37
|
import { notifyApiError } from "../../shared/notifications/apiErrors.js";
|
|
38
38
|
import { changedItemOf, useFolderLiveUpdates } from "../../shared/live/useFolderLiveUpdates.js";
|
|
39
|
+
import ProtectedMark from "../../shared/zeroAccess/ProtectedMark.js";
|
|
40
|
+
import { useKeysLocked } from "../../shared/zeroAccess/useKeysLocked.js";
|
|
41
|
+
import { isProtectedRecord, openSealedRecord, openSealedRecords } from "../../../lib/zeroAccess/sealedRecords.js";
|
|
39
42
|
|
|
40
43
|
/** The published model class name of a task on either database (`TaskMongo`, `TaskSQL`) - not a `TaskList`'s. */
|
|
41
44
|
const TASK_MODEL = /^Task(Mongo|SQL)$/;
|
|
@@ -132,6 +135,7 @@ function TasksContent() {
|
|
|
132
135
|
setLoading(true);
|
|
133
136
|
setError(null);
|
|
134
137
|
return listAllPages((page) => listTasks(folderUid, { limit: LIST_PAGE_SIZE, page }, client))
|
|
138
|
+
.then(async (result) => ({ ...result, items: await openSealedRecords(result.items) }))
|
|
135
139
|
.then((result) => {
|
|
136
140
|
if (run === loadRun.current) {
|
|
137
141
|
setTasks(result.items);
|
|
@@ -154,6 +158,15 @@ function TasksContent() {
|
|
|
154
158
|
void reload();
|
|
155
159
|
}, [folderUid]);
|
|
156
160
|
|
|
161
|
+
// The list holds protected tasks opened with the mailbox's keys: its lock takes them off screen, read again with their placeholders. They
|
|
162
|
+
// leave the list first, so a reload that fails can't leave them shown.
|
|
163
|
+
useKeysLocked((locked) => {
|
|
164
|
+
if (locked === mailboxUid) {
|
|
165
|
+
setTasks((list) => list.filter((task) => !isProtectedRecord(task)));
|
|
166
|
+
void reload();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
157
170
|
// A task created, changed or deleted anywhere - another tab, a phone over ActiveSync - is announced on its folder's channel. It is fetched again by its
|
|
158
171
|
// uid and replaces (or joins) the list while it is still in this folder; one that is gone (a delete, a 404, or moved to another folder) leaves it. A
|
|
159
172
|
// failed fetch changes nothing: the next load shows it as it is.
|
|
@@ -167,7 +180,7 @@ function TasksContent() {
|
|
|
167
180
|
remove();
|
|
168
181
|
return;
|
|
169
182
|
}
|
|
170
|
-
void getTask(change.uid, client).then(
|
|
183
|
+
void getTask(change.uid, client).then(openSealedRecord).then(
|
|
171
184
|
(fresh) => {
|
|
172
185
|
if (fresh.folderUid !== folderUid) {
|
|
173
186
|
remove();
|
|
@@ -240,7 +253,7 @@ function TasksContent() {
|
|
|
240
253
|
setCreateError("That mailbox has no Tasks folder.");
|
|
241
254
|
return;
|
|
242
255
|
}
|
|
243
|
-
const created = await createTask(
|
|
256
|
+
const created = await openSealedRecord(await createTask(
|
|
244
257
|
{
|
|
245
258
|
mailboxUid: target,
|
|
246
259
|
folderUid: targetFolderUid,
|
|
@@ -250,7 +263,7 @@ function TasksContent() {
|
|
|
250
263
|
priority,
|
|
251
264
|
},
|
|
252
265
|
client,
|
|
253
|
-
);
|
|
266
|
+
));
|
|
254
267
|
if (target === mailboxUid) {
|
|
255
268
|
setAddedElsewhere(null);
|
|
256
269
|
setTasks((prev) => [...prev, created]);
|
|
@@ -289,10 +302,12 @@ function TasksContent() {
|
|
|
289
302
|
const nextCompleted = !task.completed;
|
|
290
303
|
setTasks((prev) => prev.map((t) => (t.uid === task.uid ? { ...t, completed: nextCompleted } : t)));
|
|
291
304
|
try {
|
|
292
|
-
const updated = await setTaskCompleted(task, nextCompleted, client);
|
|
305
|
+
const updated = await openSealedRecord(await setTaskCompleted(task, nextCompleted, client));
|
|
293
306
|
setTasks((prev) => prev.map((t) => (t.uid === task.uid ? updated : t)));
|
|
294
307
|
} catch (err) {
|
|
295
|
-
|
|
308
|
+
// Only the flag goes back, on the row there now: `task` is the row as it was shown, opened, and a lock since may have
|
|
309
|
+
// reloaded it with its placeholders (#224), which putting it back whole would undo.
|
|
310
|
+
setTasks((prev) => prev.map((t) => (t.uid === task.uid ? { ...t, completed: task.completed } : t)));
|
|
296
311
|
notifyApiError(err, "Couldn't update the task");
|
|
297
312
|
} finally {
|
|
298
313
|
setTogglePending(task.uid, false);
|
|
@@ -660,6 +675,7 @@ function TaskGroup({ label, tasks, checkedUids, onToggleChecked, onToggle, toggl
|
|
|
660
675
|
<CompletionToggle task={task} onToggle={onToggle} pending={toggling.has(task.uid)} />
|
|
661
676
|
<span className={["flex-1 text-sm", task.completed ? "line-through text-text-muted" : ""].join(" ")}>
|
|
662
677
|
{task.title}
|
|
678
|
+
<ProtectedMark record={task} />
|
|
663
679
|
</span>
|
|
664
680
|
{task.priority !== "normal" && (
|
|
665
681
|
<span className={["text-xs font-medium", PRIORITY_CLASS[task.priority]].join(" ")}>
|
|
@@ -742,7 +758,9 @@ function TaskTable({ tasks, checkedUids, onToggleChecked, onToggle, toggling, on
|
|
|
742
758
|
<td className="px-3 py-2">
|
|
743
759
|
<CompletionToggle task={task} onToggle={onToggle} pending={toggling.has(task.uid)} />
|
|
744
760
|
</td>
|
|
745
|
-
<td className={["px-3 py-2", task.completed ? "line-through text-text-muted" : ""].join(" ")}>{task.title}
|
|
761
|
+
<td className={["px-3 py-2", task.completed ? "line-through text-text-muted" : ""].join(" ")}>{task.title}
|
|
762
|
+
<ProtectedMark record={task} />
|
|
763
|
+
</td>
|
|
746
764
|
<td className="px-3 py-2 text-text-muted">{task.dueDate ? parseDueDate(task.dueDate).toLocaleDateString() : ""}</td>
|
|
747
765
|
<td className={["px-3 py-2", PRIORITY_CLASS[task.priority]].join(" ")}>{PRIORITY_LABEL[task.priority]}</td>
|
|
748
766
|
<td className="px-3 py-2">
|
|
@@ -182,7 +182,7 @@ function MatterDetailContent({ uid }) {
|
|
|
182
182
|
try {
|
|
183
183
|
const material = await getAccessRequestMaterial(request.uid);
|
|
184
184
|
if (seq === materialSeq.current)
|
|
185
|
-
setMaterialText(JSON.stringify(material
|
|
185
|
+
setMaterialText(JSON.stringify(material, null, 2));
|
|
186
186
|
}
|
|
187
187
|
catch (err) {
|
|
188
188
|
if (seq === materialSeq.current) {
|
|
@@ -270,5 +270,5 @@ function MatterDetailContent({ uid }) {
|
|
|
270
270
|
? "bg-surface-alt text-text-muted"
|
|
271
271
|
: "bg-success text-white",
|
|
272
272
|
].join(" "), children: request.status })] }), actionErrors[request.uid] && _jsx(Alert, { children: actionErrors[request.uid] }), _jsxs("div", { className: "flex gap-3 mt-3", children: [!matter.closedAt && request.status === "pending" && (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", className: "!w-auto", loading: actingOn[request.uid], disabled: actingOn[request.uid], onClick: () => setApproveTarget(request), children: "Approve" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: actingOn[request.uid], disabled: actingOn[request.uid], onClick: () => setDenyTarget(request), children: "Deny" })] })), !matter.closedAt && (request.status === "approved" || request.status === "fulfilled") && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => handleGetMaterial(request), children: "Get material" }))] })] }, request.uid))) })), _jsx(LoadMoreButton, { list: requests, label: "Load more access requests" })] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsxs("div", { className: "flex items-center justify-between mb-3", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide", children: "Export this matter" }), !matter.closedAt && (_jsx(Button, { type: "button", className: "!w-auto", loading: creatingExport, disabled: creatingExport, onClick: handleCreateExport, children: "+ New export" }))] }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Exports messages (narrowed to this matter\u2019s own date range), plus full contacts, calendar events, tasks, and notes, from every custodian mailbox actually assigned to this matter\u2019s escrow scope, as one combined NDJSON file." }), exportError && _jsx(Alert, { children: exportError }), exportRequests.loadError && _jsx(Alert, { children: exportRequests.loadError }), matterExportRequests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: exportRequests.loading ? "Loading…" : "No export requests yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: matterExportRequests.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [new Date(request.dateCreated).toLocaleString(), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsxs("span", { className: "flex items-center gap-3", children: [_jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) }), !matter.closedAt && request.status === "ready" && (_jsx("a", { href: matterExportRequestDownloadUrl(request.uid), className: "text-primary-dark hover:underline font-medium", children: "Download" }))] })] }, request.uid))) }))] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-3", children: "Search this matter\u2019s custodians" }), matter.closedAt ? (_jsx("p", { className: "text-sm text-text-muted", children: "This matter is closed, so its custodians can no longer be searched or exported." })) : (_jsxs("form", { onSubmit: handleSearch, className: "flex gap-3 mb-4", children: [_jsx("input", { "aria-label": "Search this matter", className: INPUT_CLASS, value: searchText, onChange: (e) => setSearchText(e.target.value), placeholder: "Free text or an operator filter, e.g. from:alice@example.com" }), _jsx(Button, { type: "submit", className: "!w-auto shrink-0", loading: searching, disabled: searching || !searchText.trim(), children: "Search" })] })), searchError && _jsx(Alert, { children: searchError }), searchResults &&
|
|
273
|
-
(Object.keys(searchResults).length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No custodian mailboxes could be searched." })) : (_jsx("div", { className: "flex flex-col gap-4", children: Object.entries(searchResults).map(([mailboxUid, page]) => (_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold mb-2", children: mailboxUid }), page.results.length === 0 ? (_jsx("p", { className: "text-xs text-text-muted", children: "No matches." })) : (_jsx("ul", { className: "flex flex-col gap-1", children: page.results.map((result) => (_jsxs("li", { className: "text-sm py-1 px-2 bg-surface-alt rounded-sm", children: [result.entityType, " \u00B7 ", result.entityUid, result.snippet && _jsxs("span", { className: "text-text-muted", children: [" \u2014 ", result.snippet] })] }, `${result.entityType}-${result.entityUid}`))) }))] }, mailboxUid))) })))] }), _jsxs(Modal, { open: confirmingClose, onClose: closeCloseModal, title: "Close matter", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Close ", _jsx("strong", { children: matter.name }), "? Once closed, no new access requests, exports, searches, or key material reads are allowed under it. This cannot be reopened here."] }), _jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm mb-4", children: [_jsx("dt", { className: "text-text-muted", children: "Custodians" }), _jsx("dd", { className: "break-all", children: matter.custodianMailboxUids.join(", ") }), _jsx("dt", { className: "text-text-muted", children: "Date range" }), _jsx("dd", { children: formatDateRange(matter) })] }), closeError && _jsx(Alert, { children: closeError }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: closing, onClick: closeCloseModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: closing, disabled: closing, onClick: handleClose, children: "Close matter" })] })] }), _jsxs(Modal, { open: approveTarget !== null, onClose: () => setApproveTarget(null), title: "Approve access request", children: [_jsx("p", { className: "text-sm mb-3", children: "Approving lets holders of this matter\u2019s escrow scope read the key material for this mailbox once enough holders have approved." }), _jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm mb-4", children: [_jsx("dt", { className: "text-text-muted", children: "Matter" }), _jsx("dd", { children: matter.name }), _jsx("dt", { className: "text-text-muted", children: "Mailbox" }), _jsx("dd", { className: "break-all", children: approveTarget?.mailboxUid }), _jsx("dt", { className: "text-text-muted", children: "Date range" }), _jsx("dd", { children: formatDateRange(matter) }), _jsx("dt", { className: "text-text-muted", children: "Approvals" }), _jsxs("dd", { children: [approveTarget?.approvals.length, " of ", approveTarget?.requiredHoldersAtCreation, " so far, including requester ", approveTarget?.requestedByUserUid] })] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setApproveTarget(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => void handleApprove(), children: "Approve request" })] })] }), _jsxs(Modal, { open: denyTarget !== null, onClose: () => setDenyTarget(null), title: "Deny access request", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Denying refuses this request for good: it can\u2019t be approved afterwards, and holders can\u2019t read the key material for mailbox ", _jsx("span", { className: "break-all font-semibold", children: denyTarget?.mailboxUid }), " through it. A new request has to be made to ask again."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setDenyTarget(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => void handleDeny(), children: "Deny request" })] })] }), _jsx(Modal, { open: showNewRequest, onClose: () => setShowNewRequest(false), title: "New access request", children: _jsxs("form", { onSubmit: handleCreateRequest, className: "flex flex-col gap-4", children: [newRequestError && _jsx(Alert, { children: newRequestError }), _jsx(FormField, { label: "Mailbox uid", htmlFor: "newRequestMailboxUid", children: _jsx("input", { id: "newRequestMailboxUid", type: "text", className: INPUT_CLASS, value: newRequestMailboxUid, onChange: (e) => setNewRequestMailboxUid(e.target.value) }) }), _jsx("p", { className: "text-xs text-text-muted", children: "Must be one of this matter's own custodian mailboxes, currently assigned to this matter's escrow scope. Your own creation counts as the first approval." }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: creatingRequest, onClick: () => setShowNewRequest(false), children: "Cancel" }), _jsx(Button, { type: "submit", className: "!w-auto", loading: creatingRequest, disabled: creatingRequest, children: "Create request" })] })] }) }), _jsx(Modal, { open: materialFor !== null, onClose: closeMaterialModal, title: "Escrow key material", children: loadingMaterial ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : materialError ? (_jsx(Alert, { children: materialError })) : (_jsxs(_Fragment, { children: [_jsxs("p", { className: "text-sm text-text-muted mb-3", children: ["Still-encrypted
|
|
273
|
+
(Object.keys(searchResults).length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No custodian mailboxes could be searched." })) : (_jsx("div", { className: "flex flex-col gap-4", children: Object.entries(searchResults).map(([mailboxUid, page]) => (_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold mb-2", children: mailboxUid }), page.results.length === 0 ? (_jsx("p", { className: "text-xs text-text-muted", children: "No matches." })) : (_jsx("ul", { className: "flex flex-col gap-1", children: page.results.map((result) => (_jsxs("li", { className: "text-sm py-1 px-2 bg-surface-alt rounded-sm", children: [result.entityType, " \u00B7 ", result.entityUid, result.snippet && _jsxs("span", { className: "text-text-muted", children: [" \u2014 ", result.snippet] })] }, `${result.entityType}-${result.entityUid}`))) }))] }, mailboxUid))) })))] }), _jsxs(Modal, { open: confirmingClose, onClose: closeCloseModal, title: "Close matter", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Close ", _jsx("strong", { children: matter.name }), "? Once closed, no new access requests, exports, searches, or key material reads are allowed under it. This cannot be reopened here."] }), _jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm mb-4", children: [_jsx("dt", { className: "text-text-muted", children: "Custodians" }), _jsx("dd", { className: "break-all", children: matter.custodianMailboxUids.join(", ") }), _jsx("dt", { className: "text-text-muted", children: "Date range" }), _jsx("dd", { children: formatDateRange(matter) })] }), closeError && _jsx(Alert, { children: closeError }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: closing, onClick: closeCloseModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: closing, disabled: closing, onClick: handleClose, children: "Close matter" })] })] }), _jsxs(Modal, { open: approveTarget !== null, onClose: () => setApproveTarget(null), title: "Approve access request", children: [_jsx("p", { className: "text-sm mb-3", children: "Approving lets holders of this matter\u2019s escrow scope read the key material for this mailbox once enough holders have approved." }), _jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm mb-4", children: [_jsx("dt", { className: "text-text-muted", children: "Matter" }), _jsx("dd", { children: matter.name }), _jsx("dt", { className: "text-text-muted", children: "Mailbox" }), _jsx("dd", { className: "break-all", children: approveTarget?.mailboxUid }), _jsx("dt", { className: "text-text-muted", children: "Date range" }), _jsx("dd", { children: formatDateRange(matter) }), _jsx("dt", { className: "text-text-muted", children: "Approvals" }), _jsxs("dd", { children: [approveTarget?.approvals.length, " of ", approveTarget?.requiredHoldersAtCreation, " so far, including requester ", approveTarget?.requestedByUserUid] })] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setApproveTarget(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => void handleApprove(), children: "Approve request" })] })] }), _jsxs(Modal, { open: denyTarget !== null, onClose: () => setDenyTarget(null), title: "Deny access request", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Denying refuses this request for good: it can\u2019t be approved afterwards, and holders can\u2019t read the key material for mailbox ", _jsx("span", { className: "break-all font-semibold", children: denyTarget?.mailboxUid }), " through it. A new request has to be made to ask again."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setDenyTarget(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => void handleDeny(), children: "Deny request" })] })] }), _jsx(Modal, { open: showNewRequest, onClose: () => setShowNewRequest(false), title: "New access request", children: _jsxs("form", { onSubmit: handleCreateRequest, className: "flex flex-col gap-4", children: [newRequestError && _jsx(Alert, { children: newRequestError }), _jsx(FormField, { label: "Mailbox uid", htmlFor: "newRequestMailboxUid", children: _jsx("input", { id: "newRequestMailboxUid", type: "text", className: INPUT_CLASS, value: newRequestMailboxUid, onChange: (e) => setNewRequestMailboxUid(e.target.value) }) }), _jsx("p", { className: "text-xs text-text-muted", children: "Must be one of this matter's own custodian mailboxes, currently assigned to this matter's escrow scope. Your own creation counts as the first approval." }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: creatingRequest, onClick: () => setShowNewRequest(false), children: "Cancel" }), _jsx(Button, { type: "submit", className: "!w-auto", loading: creatingRequest, disabled: creatingRequest, children: "Create request" })] })] }) }), _jsx(Modal, { open: materialFor !== null, onClose: closeMaterialModal, title: "Escrow key material", children: loadingMaterial ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : materialError ? (_jsx(Alert, { children: materialError })) : (_jsxs(_Fragment, { children: [_jsxs("p", { className: "text-sm text-text-muted mb-3", children: ["Still-encrypted key material for ", _jsx("strong", { children: materialFor?.mailboxUid }), ": the escrow master key wrap, and the private keys that read its mail, sealed under that master key. This server cannot decrypt any of it either. Copy the JSON below into your own offline tooling."] }), _jsx("pre", { className: "text-xs font-mono bg-surface-alt border border-border rounded-sm p-3 overflow-x-auto whitespace-pre-wrap break-all", children: materialText })] })) })] }));
|
|
274
274
|
}
|
|
@@ -12,7 +12,21 @@ export interface CalendarReminderNotice {
|
|
|
12
12
|
/** The event's own `location`, verbatim - `undefined`/`null` when the event has none. Not necessarily a URL;
|
|
13
13
|
* see `joinMeetingUrl()` for when it's shown as a join link. */
|
|
14
14
|
location?: string | null;
|
|
15
|
+
/** The event is of a calendar zero-access encryption protects, or was made from a zero-access invitation: `title` and `location` are
|
|
16
|
+
* placeholders (`openedReminder()`). */
|
|
17
|
+
protected?: boolean;
|
|
18
|
+
/** Set by `openedReminder()` on a protected notice it opened: the placeholder title the push carried, for anything recorded beyond
|
|
19
|
+
* this tab's memory (the notification history is kept in `sessionStorage` and outlives a mailbox lock). */
|
|
20
|
+
sealedTitle?: string;
|
|
21
|
+
/** Set with `sealedTitle`: the placeholder location the push carried, which `sealedReminder()` puts back. */
|
|
22
|
+
sealedLocation?: string | null;
|
|
15
23
|
}
|
|
24
|
+
/** `notice` with a protected event's real title and location, read from the event itself (opened with the mailbox's keys); `notice` as it
|
|
25
|
+
* came when the event can't be read or opened - the alarm still goes off, with the placeholder. Never rejects. */
|
|
26
|
+
export declare function openedReminder(notice: CalendarReminderNotice): Promise<CalendarReminderNotice>;
|
|
27
|
+
/** `notice` as the push carried it, when `openedReminder()` opened it - for an alarm still shown, or snoozed, when the mailbox locks: its real
|
|
28
|
+
* title and location go with the keys that opened them. `openedReminder()` opens it again (it is still `protected`). */
|
|
29
|
+
export declare function sealedReminder(notice: CalendarReminderNotice): CalendarReminderNotice;
|
|
16
30
|
/** A `notice.location` worth offering as a one-click "Join Meeting" link - a plain `http(s)://` URL and nothing
|
|
17
31
|
* else (a room name, an address, free-text instructions are all real, common `location` values this must not
|
|
18
32
|
* treat as a link). Trimmed first, so incidental whitespace never hides a real link. */
|