@rapidmx/web-client 0.36.0 → 0.37.1
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/shared/calendar/calendarReminders.ts +35 -0
- package/apps/shared/calendar/sealedEvents.ts +42 -9
- 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 +29 -1
- 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 +6 -3
- 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 +11 -4
- package/apps/shared/components/layout/SharedMailboxKeyManager.tsx +37 -6
- package/apps/shared/components/mail/InviteCard.tsx +11 -1
- package/apps/shared/components/mail/MessageDetailPane.tsx +25 -14
- 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/protectedContactSuggestions.ts +121 -0
- package/apps/shared/components/mail/invite/inviteStore.ts +11 -1
- package/apps/shared/components/mail/pinnedSigners.ts +14 -0
- package/apps/shared/components/mail/reading/conversationPrint.ts +3 -3
- package/apps/shared/components/settings/KeyExportSetting.tsx +4 -1
- package/apps/shared/components/settings/OpenPgpKeySetting.tsx +240 -0
- package/apps/shared/components/settings/ZeroAccessSetting.tsx +13 -2
- package/apps/shared/components/settings/ZeroAccessTypes.tsx +175 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +9 -2
- package/apps/shared/mail/outbox/sendJob.ts +349 -50
- package/apps/shared/mail/outbox/sendOutcomes.ts +91 -20
- package/apps/shared/search/LocalIndexLifecycle.tsx +62 -33
- package/apps/shared/search/localIndexBuilder.ts +35 -24
- package/apps/shared/search/localIndexRpcClient.ts +91 -11
- package/apps/shared/search/localIndexSchema.ts +18 -2
- package/apps/shared/search/localIndexTransport.ts +6 -3
- package/apps/shared/search/localIndexWorker.ts +35 -18
- package/apps/shared/search/searchTier2.ts +5 -0
- package/apps/shared/search/zeroAccessIndexSync.ts +232 -40
- 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 +49 -5
- package/apps/www/contacts/index.tsx +67 -11
- package/apps/www/index.tsx +10 -1
- package/apps/www/settings/encryption/index.tsx +16 -1
- package/apps/www/settings/privacy/index.tsx +87 -38
- package/apps/www/tasks/index.tsx +24 -6
- 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 +3 -2
- package/dist/apps/shared/calendar/sealedEvents.js +40 -9
- 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 +9 -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 +6 -3
- 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 +10 -4
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.d.ts +3 -1
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.js +30 -4
- package/dist/apps/shared/components/mail/InviteCard.js +11 -1
- package/dist/apps/shared/components/mail/MessageDetailPane.js +15 -11
- 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/protectedContactSuggestions.d.ts +14 -0
- package/dist/apps/shared/components/mail/compose/protectedContactSuggestions.js +96 -0
- package/dist/apps/shared/components/mail/invite/inviteStore.js +11 -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/settings/KeyExportSetting.js +3 -1
- 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/ZeroAccessSetting.d.ts +2 -0
- package/dist/apps/shared/components/settings/ZeroAccessSetting.js +12 -3
- 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/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/search/LocalIndexLifecycle.js +55 -33
- package/dist/apps/shared/search/localIndexBuilder.d.ts +4 -1
- package/dist/apps/shared/search/localIndexBuilder.js +31 -20
- package/dist/apps/shared/search/localIndexRpcClient.d.ts +5 -39
- package/dist/apps/shared/search/localIndexRpcClient.js +126 -10
- package/dist/apps/shared/search/localIndexSchema.d.ts +14 -2
- package/dist/apps/shared/search/localIndexSchema.js +16 -2
- package/dist/apps/shared/search/localIndexTransport.d.ts +6 -3
- package/dist/apps/shared/search/localIndexWorker.d.ts +9 -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 +37 -6
- package/dist/apps/shared/search/zeroAccessIndexSync.js +199 -37
- 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 +47 -5
- package/dist/apps/www/contacts/index.js +64 -11
- package/dist/apps/www/index.js +10 -1
- package/dist/apps/www/settings/encryption/index.js +14 -4
- package/dist/apps/www/settings/privacy/index.js +46 -5
- package/dist/apps/www/tasks/index.js +21 -7
- package/dist/lib/admin/domainsApi.d.ts +1 -1
- package/dist/lib/calendar/calendarApi.d.ts +46 -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 +61 -4
- 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 +6 -8
- package/dist/lib/calendar/sealedInvites.js +43 -6
- 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.js +2 -2
- package/dist/lib/crypto/keyPairMatch.d.ts +9 -0
- package/dist/lib/crypto/keyPairMatch.js +65 -0
- package/dist/lib/crypto/keySession.d.ts +19 -2
- package/dist/lib/crypto/keySession.js +89 -13
- package/dist/lib/crypto/keyvaultApi.d.ts +41 -5
- package/dist/lib/crypto/keyvaultApi.js +13 -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 +26 -2
- package/dist/lib/crypto/messageSecurity.js +122 -10
- 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 +15 -3
- package/dist/lib/crypto/sharedMailboxKeys.js +102 -29
- package/dist/lib/crypto/smimeMessage.d.ts +21 -0
- package/dist/lib/crypto/smimeMessage.js +11 -7
- package/dist/lib/crypto/zeroAccessKey.js +5 -0
- package/dist/lib/mail/mailApi.d.ts +33 -0
- package/dist/lib/mail/mailboxImportApi.d.ts +10 -0
- package/dist/lib/mail/mailboxImportApi.js +15 -0
- package/dist/lib/search/zeroAccessIndexApi.d.ts +4 -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/calendar/calendarApi.ts +140 -17
- package/lib/calendar/calendarMutations.ts +79 -12
- package/lib/calendar/inviteApi.ts +75 -5
- package/lib/calendar/protectedEvents.ts +143 -0
- package/lib/calendar/recurrence.ts +17 -0
- package/lib/calendar/sealedInvites.ts +47 -7
- 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 +2 -2
- package/lib/crypto/keyPairMatch.ts +69 -0
- package/lib/crypto/keySession.ts +102 -12
- package/lib/crypto/keyvaultApi.ts +49 -7
- package/lib/crypto/meetingKeys.ts +118 -0
- package/lib/crypto/messageSecurity.ts +174 -13
- 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 +106 -28
- package/lib/crypto/smimeMessage.ts +11 -7
- package/lib/crypto/zeroAccessKey.ts +5 -0
- package/lib/mail/mailApi.ts +27 -0
- package/lib/mail/mailboxImportApi.ts +24 -0
- package/lib/search/zeroAccessIndexApi.ts +4 -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 +12 -3
|
@@ -24,10 +24,13 @@ import {
|
|
|
24
24
|
import { ApiRequestError } from "../../../lib/util/api.js";
|
|
25
25
|
import { useApiClient } from "../../../lib/util/apiClientContext.js";
|
|
26
26
|
import useIsMobile from "../../../lib/util/useIsMobile.js";
|
|
27
|
-
import { CalendarEvent, getCalendarEvent, listCalendarEvents } from "../../../lib/calendar/calendarApi.js";
|
|
27
|
+
import { CalendarEvent, fullyOpenedEvent, getCalendarEvent, listCalendarEvents } from "../../../lib/calendar/calendarApi.js";
|
|
28
|
+
import { isOpenedRecord, isProtectedRecord } from "../../../lib/zeroAccess/sealedRecords.js";
|
|
29
|
+
import { isSealedRecord } from "../../../lib/zeroAccess/sealedMoves.js";
|
|
30
|
+
import { useKeysLocked } from "../../shared/zeroAccess/useKeysLocked.js";
|
|
28
31
|
import { openSealedEvent, openSealedEvents } from "../../shared/calendar/sealedEvents.js";
|
|
29
32
|
import { useFolderLiveUpdates } from "../../shared/live/useFolderLiveUpdates.js";
|
|
30
|
-
import { moveOccurrence, resizeOccurrenceEnd } from "../../../lib/calendar/calendarMutations.js";
|
|
33
|
+
import { SealedOccurrenceError, moveOccurrence, resizeOccurrenceEnd } from "../../../lib/calendar/calendarMutations.js";
|
|
31
34
|
import { resolveDragAction } from "../../../lib/calendar/calendarDragIds.js";
|
|
32
35
|
import { createFolder } from "../../../lib/mail/mailApi.js";
|
|
33
36
|
import { CalendarOccurrence, expandAllOccurrences, expandOccurrences } from "../../../lib/calendar/recurrence.js";
|
|
@@ -77,6 +80,8 @@ import { SHORTCUTS, ShortcutDef } from "../../shared/keyboard/keymap.js";
|
|
|
77
80
|
import { useShortcut } from "../../shared/keyboard/useShortcut.js";
|
|
78
81
|
import { useShortcutProps } from "../../shared/keyboard/useShortcutProps.js";
|
|
79
82
|
import { notifyApiError } from "../../shared/notifications/apiErrors.js";
|
|
83
|
+
import { notify } from "../../shared/notifications/store.js";
|
|
84
|
+
import { KeysLockedError } from "../../../lib/crypto/masterKey.js";
|
|
80
85
|
import { bookingSettingsHref } from "../../shared/calendar/bookingPlugin.js";
|
|
81
86
|
import { changedEventOf } from "../../shared/calendar/calendarLiveUpdates.js";
|
|
82
87
|
|
|
@@ -390,11 +395,44 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
390
395
|
// Today moves on at midnight: the List's "from today on" is worked out again then (`listDay` is only a dependency).
|
|
391
396
|
const listDay = useDayKey();
|
|
392
397
|
const dayOfList = view === "list" ? listDay : "";
|
|
398
|
+
// A search matches descriptions too, which a list leaves sealed (`EVENT_LIST_FIELDS`): the protected events are opened in full once one
|
|
399
|
+
// runs, and searched as they are until then.
|
|
400
|
+
const [searchable, setSearchable] = useState<{ from: CalendarEvent[]; events: CalendarEvent[] } | null>(null);
|
|
401
|
+
useEffect(() => {
|
|
402
|
+
if (!searching || !events.some((event) => isProtectedRecord(event) && !isOpenedRecord(event))) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
let cancelled = false;
|
|
406
|
+
void Promise.all(events.map((event) => fullyOpenedEvent(event))).then((opened) => {
|
|
407
|
+
if (!cancelled) {
|
|
408
|
+
setSearchable({ from: events, events: opened });
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
return () => {
|
|
412
|
+
cancelled = true;
|
|
413
|
+
};
|
|
414
|
+
}, [events, searching]);
|
|
415
|
+
const searchedEvents = searchable?.from === events ? searchable.events : events;
|
|
416
|
+
|
|
417
|
+
// The calendar holds protected events opened with their mailboxes' keys: a lock of one takes them off screen - read again with their
|
|
418
|
+
// placeholders, the search's fully opened copies dropped, and an open card or form of a protected event closed - its own mailbox's
|
|
419
|
+
// lock closes it too when it isn't one of the listed events (opened from a reminder, its calendar unchecked).
|
|
420
|
+
useKeysLocked((locked) => {
|
|
421
|
+
const listed: boolean = events.some((event) => event.mailboxUid === locked);
|
|
422
|
+
setModal((current) =>
|
|
423
|
+
current?.occurrence && isSealedRecord(current.occurrence) && (listed || current.occurrence.mailboxUid === locked) ? null : current,
|
|
424
|
+
);
|
|
425
|
+
if (!listed) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
reload();
|
|
429
|
+
setSearchable(null);
|
|
430
|
+
});
|
|
393
431
|
const matches = useMemo(() => {
|
|
394
|
-
const found = searching ? searchOccurrences(
|
|
432
|
+
const found = searching ? searchOccurrences(searchedEvents, query) : [];
|
|
395
433
|
const today = new Date();
|
|
396
434
|
return view === "list" ? found.filter((occurrence) => isUpcoming(occurrence, today)) : found;
|
|
397
|
-
}, [
|
|
435
|
+
}, [searchedEvents, query, searching, view, dayOfList]);
|
|
398
436
|
const activeIndex = matches.findIndex((m) => m.occurrenceKey === activeKey);
|
|
399
437
|
|
|
400
438
|
// What an open event card steps through, in order: the matches while a search runs, else every event of the checked calendars (bounded as a
|
|
@@ -684,7 +722,13 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
684
722
|
}
|
|
685
723
|
reload();
|
|
686
724
|
} catch (err) {
|
|
687
|
-
|
|
725
|
+
const context = action.type === "move" ? "Couldn't move the event" : "Couldn't resize the event";
|
|
726
|
+
if (err instanceof KeysLockedError || err instanceof SealedOccurrenceError) {
|
|
727
|
+
// A protected occurrence while the mailbox is locked, or one of a zero-access invitation: nothing was written.
|
|
728
|
+
notify({ kind: "warning", title: context, message: err.message });
|
|
729
|
+
} else {
|
|
730
|
+
notifyApiError(err, context);
|
|
731
|
+
}
|
|
688
732
|
// A refused change (a 409 most of all) means the page holds a stale version: load again, or every later drag of this event fails the same way.
|
|
689
733
|
reload();
|
|
690
734
|
}
|
|
@@ -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,6 +159,9 @@ 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
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
|
|
164
167
|
// no encrypted mail in it never needs it.
|
|
@@ -167,11 +170,17 @@ async function decryptEncryptedRows(messages: Message[], unlocked: UnlockedKeys)
|
|
|
167
170
|
const summary = message.zeroAccess ? await securityApi.openZeroAccessSummary(message.zeroAccess.sealedSummary, unlocked.zeroAccessKeys) : undefined;
|
|
168
171
|
if (summary) {
|
|
169
172
|
const preview = summary.preview.slice(0, LIST_PREVIEW_MAX_LENGTH);
|
|
170
|
-
|
|
173
|
+
if (stillUnlocked()) {
|
|
174
|
+
rememberDecrypted(message.uid, message.mailboxUid, { subject: summary.subject, preview });
|
|
175
|
+
}
|
|
171
176
|
return [message.uid, { subject: summary.subject, preview }];
|
|
172
177
|
}
|
|
173
178
|
const rawMime = await getMessageRawContent(message.uid);
|
|
174
179
|
const security = await securityApi.evaluateMessageSecurity(rawMime, unlocked);
|
|
180
|
+
// Nor is a lock a failure to decrypt.
|
|
181
|
+
if (!stillUnlocked()) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
175
184
|
if (!security.subject && !security.html) {
|
|
176
185
|
markDecryptFailed(message.uid);
|
|
177
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,
|
|
@@ -53,6 +56,7 @@ import Button from "../../../../lib/components/buttons/Button.js";
|
|
|
53
56
|
import Modal from "../../../../lib/components/overlays/Modal.js";
|
|
54
57
|
import ZeroAccessSetting from "../../../shared/components/settings/ZeroAccessSetting.js";
|
|
55
58
|
import KeyExportSetting from "../../../shared/components/settings/KeyExportSetting.js";
|
|
59
|
+
import OpenPgpKeySetting from "../../../shared/components/settings/OpenPgpKeySetting.js";
|
|
56
60
|
import SigningCertificateCard, { SigningCertificateMode } from "../../../shared/components/settings/SigningCertificateCard.js";
|
|
57
61
|
import { readStoredSignEnrollment, storeSignEnrollment } from "../../../shared/signing/enrollmentStorage.js";
|
|
58
62
|
import {
|
|
@@ -103,7 +107,14 @@ class StaleSessionKeysError extends Error {
|
|
|
103
107
|
const STALE_KEYS_MESSAGE =
|
|
104
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.";
|
|
105
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. */
|
|
106
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
|
+
}
|
|
107
118
|
return buildAad(mailboxUid, entry.useType === "sign" ? SIGNING_PRIVATE_KEY_AAD_PURPOSE : ENCRYPTION_PRIVATE_KEY_AAD_PURPOSE);
|
|
108
119
|
}
|
|
109
120
|
|
|
@@ -169,6 +180,8 @@ async function rewrapVaultPrivateKeys(
|
|
|
169
180
|
algorithm: "AES-256-GCM",
|
|
170
181
|
fingerprint: entry.fingerprint,
|
|
171
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 } : {}),
|
|
172
185
|
});
|
|
173
186
|
}
|
|
174
187
|
return { mk, wrappedKeys: rewrapped };
|
|
@@ -229,7 +242,8 @@ function EncryptionGate({ userUid, impersonating }: { userUid?: string; imperson
|
|
|
229
242
|
mailboxKeys={mailbox.keys}
|
|
230
243
|
canProvision={isOwner}
|
|
231
244
|
>
|
|
232
|
-
|
|
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} />
|
|
233
247
|
</KeyEnrollmentGate>
|
|
234
248
|
);
|
|
235
249
|
}
|
|
@@ -979,6 +993,7 @@ function EncryptionContent({ canManageKeys }: { canManageKeys: boolean }) {
|
|
|
979
993
|
|
|
980
994
|
<ZeroAccessSetting mailbox={mailbox} canManage={canManageKeys} />
|
|
981
995
|
<KeyExportSetting mailbox={mailbox} canManage={canManageKeys} />
|
|
996
|
+
<OpenPgpKeySetting mailbox={mailbox} canManage={canManageKeys} />
|
|
982
997
|
|
|
983
998
|
<div>
|
|
984
999
|
<h2 className="text-sm font-semibold mb-2">Digital signatures</h2>
|
|
@@ -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
|
) : (
|
|
@@ -238,8 +242,36 @@ function ExportSection({ mailboxUid, mailboxLabel }: { mailboxUid?: string; mail
|
|
|
238
242
|
);
|
|
239
243
|
}
|
|
240
244
|
|
|
241
|
-
|
|
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 }) {
|
|
242
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);
|
|
243
275
|
const [folders, setFolders] = useState<Folder[]>([]);
|
|
244
276
|
const [targetFolderUid, setTargetFolderUid] = useState("");
|
|
245
277
|
const [foldersError, setFoldersError] = useState<string | null>(null);
|
|
@@ -294,7 +326,18 @@ function ImportSection({ mailboxUid }: { mailboxUid?: string }) {
|
|
|
294
326
|
setUploading(true);
|
|
295
327
|
uploadMailboxImport(file, { format: importFormatFromFilename(file.name), targetFolderUid, mailboxUid: mailboxUid! }, client)
|
|
296
328
|
.then(() => loadRequests())
|
|
297
|
-
.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
|
+
})
|
|
298
341
|
.finally(() => setUploading(false));
|
|
299
342
|
}
|
|
300
343
|
|
|
@@ -306,43 +349,49 @@ function ImportSection({ mailboxUid }: { mailboxUid?: string }) {
|
|
|
306
349
|
choose below. This runs in the background — check back here for its progress.
|
|
307
350
|
</p>
|
|
308
351
|
|
|
309
|
-
{
|
|
310
|
-
<
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
className="
|
|
315
|
-
|
|
316
|
-
|
|
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()}
|
|
317
381
|
>
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
+
</>
|
|
325
393
|
)}
|
|
326
394
|
|
|
327
|
-
<Button
|
|
328
|
-
type="button"
|
|
329
|
-
variant="secondary"
|
|
330
|
-
className="!w-auto mb-3"
|
|
331
|
-
loading={uploading}
|
|
332
|
-
disabled={uploading || !targetFolderUid}
|
|
333
|
-
onClick={() => fileInputRef.current?.click()}
|
|
334
|
-
>
|
|
335
|
-
Upload Mbox or PST file
|
|
336
|
-
</Button>
|
|
337
|
-
<input
|
|
338
|
-
ref={fileInputRef}
|
|
339
|
-
type="file"
|
|
340
|
-
accept=".mbox,.pst"
|
|
341
|
-
aria-label="Upload mail archive"
|
|
342
|
-
className="hidden"
|
|
343
|
-
onChange={handleFileChange}
|
|
344
|
-
/>
|
|
345
|
-
|
|
346
395
|
{foldersError && <Alert>{foldersError}</Alert>}
|
|
347
396
|
{loadError && <Alert>{loadError}</Alert>}
|
|
348
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">
|
|
@@ -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. */
|