@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
|
@@ -6,8 +6,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
6
6
|
import { pageTitle } from "../../../shared/navigation/pageTitle.js";
|
|
7
7
|
import { useEffect, useState } from "react";
|
|
8
8
|
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
9
|
-
import { addMasterKeyWrap, cancelSignEnrollment, findActivePublicKey, getCurrentSignEnrollment, getEscrowInfo, getKeyVault, rekey, removeMasterKeyWrap, startSignEnrollment, } from "../../../../lib/crypto/keyvaultApi.js";
|
|
10
|
-
import { ENCRYPTION_PRIVATE_KEY_AAD_PURPOSE, SIGNING_PRIVATE_KEY_AAD_PURPOSE, destroyUnlockedKeys, getUnlockedKeys, unlockWithPassword, } from "../../../../lib/crypto/keySession.js";
|
|
9
|
+
import { addMasterKeyWrap, cancelSignEnrollment, findActivePublicKey, getCurrentSignEnrollment, getEscrowInfo, getKeyVault, isOpenPgpKey, rekey, removeMasterKeyWrap, startSignEnrollment, } from "../../../../lib/crypto/keyvaultApi.js";
|
|
10
|
+
import { ENCRYPTION_PRIVATE_KEY_AAD_PURPOSE, OPENPGP_PRIVATE_KEY_AAD_PURPOSE, SIGNING_PRIVATE_KEY_AAD_PURPOSE, ZERO_ACCESS_PRIVATE_KEY_AAD_PURPOSE, destroyUnlockedKeys, getUnlockedKeys, unlockWithPassword, } from "../../../../lib/crypto/keySession.js";
|
|
11
11
|
import { IDLE_TIMEOUT_OPTIONS_MINUTES, getIdleTimeoutMinutes, setIdleTimeoutMinutes } from "../../../../lib/crypto/idleTimeout.js";
|
|
12
12
|
import { LOCAL_INDEX_SIZE_OPTIONS, getDefaultLocalIndexByteBudget, getLocalIndexByteBudget, setLocalIndexByteBudget, } from "../../../shared/search/localIndexSizePreference.js";
|
|
13
13
|
import { fromBase64 } from "../../../../lib/crypto/encoding.js";
|
|
@@ -25,6 +25,9 @@ import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
|
|
|
25
25
|
import Alert from "../../../../lib/components/feedback/Alert.js";
|
|
26
26
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
27
27
|
import Modal from "../../../../lib/components/overlays/Modal.js";
|
|
28
|
+
import ZeroAccessSetting from "../../../shared/components/settings/ZeroAccessSetting.js";
|
|
29
|
+
import KeyExportSetting from "../../../shared/components/settings/KeyExportSetting.js";
|
|
30
|
+
import OpenPgpKeySetting from "../../../shared/components/settings/OpenPgpKeySetting.js";
|
|
28
31
|
import SigningCertificateCard from "../../../shared/components/settings/SigningCertificateCard.js";
|
|
29
32
|
import { readStoredSignEnrollment, storeSignEnrollment } from "../../../shared/signing/enrollmentStorage.js";
|
|
30
33
|
import { checkEnrollmentNow, forgetEnrollment, recordEnrollmentResult, useEnrollmentSnapshot, watchCurrentEnrollment, watchEnrollment, } from "../../../shared/signing/enrollmentTracker.js";
|
|
@@ -61,7 +64,14 @@ class StaleSessionKeysError extends Error {
|
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
66
|
const STALE_KEYS_MESSAGE = "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.";
|
|
67
|
+
/** 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. */
|
|
64
68
|
function wrappedKeyAad(mailboxUid, entry) {
|
|
69
|
+
if (isOpenPgpKey(entry)) {
|
|
70
|
+
return buildAad(mailboxUid, OPENPGP_PRIVATE_KEY_AAD_PURPOSE);
|
|
71
|
+
}
|
|
72
|
+
if (entry.useType === "zero-access") {
|
|
73
|
+
return buildAad(mailboxUid, ZERO_ACCESS_PRIVATE_KEY_AAD_PURPOSE);
|
|
74
|
+
}
|
|
65
75
|
return buildAad(mailboxUid, entry.useType === "sign" ? SIGNING_PRIVATE_KEY_AAD_PURPOSE : ENCRYPTION_PRIVATE_KEY_AAD_PURPOSE);
|
|
66
76
|
}
|
|
67
77
|
/**
|
|
@@ -123,6 +133,8 @@ async function rewrapVaultPrivateKeys(mailboxUid, currentMasterKey, wrappedKeys)
|
|
|
123
133
|
algorithm: "AES-256-GCM",
|
|
124
134
|
fingerprint: entry.fingerprint,
|
|
125
135
|
useType: entry.useType,
|
|
136
|
+
// An OpenPGP secret key stays one: without its type it would read back as an S/MIME key.
|
|
137
|
+
...(isOpenPgpKey(entry) ? { type: entry.type } : {}),
|
|
126
138
|
});
|
|
127
139
|
}
|
|
128
140
|
return { mk, wrappedKeys: rewrapped };
|
|
@@ -165,7 +177,7 @@ function EncryptionGate({ userUid, impersonating }) {
|
|
|
165
177
|
// restapi only lets the mailbox's actual owner write its key vault (`requireMailboxOwner()`), so a
|
|
166
178
|
// delegate (or an admin impersonating the owner) never provisions keys here, and sees no write actions.
|
|
167
179
|
const isOwner = mailbox.ownerUserUid !== undefined && mailbox.ownerUserUid === userUid && !impersonating;
|
|
168
|
-
return (_jsx(KeyEnrollmentGate, { mailboxUid: mailboxUid, mailboxAddress: mailbox.primarySmtpAddress, mailboxKeys: mailbox.keys, canProvision: isOwner, children: _jsx(EncryptionContent, { canManageKeys: isOwner }) }));
|
|
180
|
+
return (_jsx(KeyEnrollmentGate, { mailboxUid: mailboxUid, mailboxAddress: mailbox.primarySmtpAddress, mailboxKeys: mailbox.keys, canProvision: isOwner, children: _jsx(EncryptionContent, { canManageKeys: isOwner }, mailboxUid) }));
|
|
169
181
|
}
|
|
170
182
|
function EncryptionContent({ canManageKeys }) {
|
|
171
183
|
const { mailboxUid, mailboxes } = useSettingsShell();
|
|
@@ -799,7 +811,7 @@ function EncryptionContent({ canManageKeys }) {
|
|
|
799
811
|
: null;
|
|
800
812
|
const certificateEnrollment = certificateMode === "issued" && enrollmentResult?.status !== "issued" ? null : certificateMode === "expired" ? null : enrollmentResult;
|
|
801
813
|
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl flex flex-col gap-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Encryption" }), _jsxs("p", { className: "text-sm text-text-muted", children: ["Manage how ", mailbox.displayName, " unlocks its encryption keys on this and other devices."] })] }), loadError && _jsx(Alert, { children: loadError }), actionError && _jsx(Alert, { children: actionError }), !canManageKeys && (_jsx("p", { className: "text-sm text-text-muted", children: "Only this mailbox\u2019s owner, signed in as themselves, can change its encryption keys and unlock methods." })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Encryption keys" }), displayedKeys.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-1 text-sm", children: displayedKeys.map((key) => (_jsxs("li", { className: "font-mono text-xs", children: [key.useType === "sign" ? "Signing" : "Encryption", " key: ", key.fingerprint, key.revokedAt &&
|
|
802
|
-
(key.revocationReason === "superseded" ? (_jsx("span", { className: "text-text-muted", children: " (superseded)" })) : (_jsx("span", { className: "text-danger", children: " (revoked)" })))] }, key.fingerprint))) })) : (_jsx("p", { className: "text-sm text-text-muted", children: "No keys enrolled yet." }))] }), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Digital signatures" }), activeSigningKey && signingStatus !== "pending" && (_jsx("p", { className: ["text-sm text-text-muted", certificateMode ? "mb-3" : ""].join(" "), children: "Enabled \u2014 outgoing mail from this mailbox is signed with a publicly-trusted certificate." })), certificateMode && (_jsxs("div", { className: "flex flex-col gap-2", children: [certificateMode === "failed" && signingError && _jsx(Alert, { children: signingError }), _jsx(SigningCertificateCard, { mode: certificateMode, enrollment: certificateEnrollment, snapshot: enrollment, info: signingInfo, address: mailbox.primarySmtpAddress, keyNotAfter: certificateMode === "expired" ? expiredSigningKey?.notAfter : activeSigningKey?.notAfter, canRequest: canManageKeys, requesting: signingStatus === "enrolling", onCheck: canManageKeys ? () => void checkEnrollmentNow(mailboxUid) : undefined, onRequest: handleEnrollSigning })] })), !activeSigningKey && !certificateMode && !canManageKeys ? (_jsx("p", { className: "text-sm text-text-muted", children: "Not enabled." })) : activeSigningKey || certificateMode ? null : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-xs text-text-muted", children: "Lets recipients verify that mail from this mailbox is genuinely from you. Optional \u2014 encryption already works without it." }), beforeRequestText(signingInfo, mailbox.primarySmtpAddress) && (_jsx("p", { className: "text-xs text-text-muted", children: beforeRequestText(signingInfo, mailbox.primarySmtpAddress) })), healthWarning(signingInfo) && _jsx(Alert, { children: healthWarning(signingInfo) }), enrollment?.gone && _jsx(Alert, { children: STALE_REQUEST_TEXT }), signingError && _jsx(Alert, { children: signingError }), signingInfo?.backend !== "none" && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: signingStatus === "enrolling", disabled: signingStatus !== "idle", onClick: handleEnrollSigning, children: "Enable digital signatures" }))] }))] }), mailbox.escrowScopeId && (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Escrow" }), escrowError && _jsx(Alert, { children: escrowError }), hasEscrowWrap ? (_jsx("p", { className: "text-sm text-text-muted", children: "This mailbox is under legal/compliance escrow \u2014 an authorized holder in your organization can recover its encrypted mail if needed. This does not weaken protection against anyone else." })) : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("p", { className: "text-xs text-text-muted", children: ["Your organization has assigned this mailbox to an escrow scope, but nothing has been protected yet \u2014 an authorized holder cannot recover this mailbox\u2019s encrypted mail until you complete this step.", " ", "This does not weaken protection against anyone else."] }), canManageKeys && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: wrappingEscrow, disabled: wrappingEscrow, onClick: handleWrapEscrow, children: "Add escrow protection" }))] })), _jsxs(Modal, { open: pendingEscrow !== null, onClose: () => setPendingEscrow(null), title: "Confirm escrow certificate", children: [_jsx("p", { className: "text-sm mb-3", children: "Anyone holding the private key for this certificate will be able to recover this mailbox\u2019s encrypted mail. Confirm it is the escrow certificate your organization expects before continuing." }), _jsxs("dl", { className: "text-sm mb-5 flex flex-col gap-1", children: [_jsxs("div", { children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Escrow scope" }), _jsx("dd", { className: "break-all", children: pendingEscrow?.escrowScopeId })] }), _jsxs("div", { children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Certificate fingerprint" }), _jsx("dd", { className: "font-mono break-all", children: pendingEscrow?.publicKey.fingerprint })] })] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setPendingEscrow(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => void handleConfirmEscrow(pendingEscrow), children: "Trust this certificate" })] })] })] })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Unlock methods" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["Removing a method here stops it from being usable to unlock this mailbox going forward, but it is ", _jsx("strong", { children: "not" }), " full revocation \u2014 anyone who already captured a wrapped copy and knows its secret could still use it. For real revocation (e.g. after a lost device), rotate your keys entirely below instead."] }), vault && vault.masterKeyWraps.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-2", children: [...passwordWraps, ...recoveryWraps, ...otherWraps].map((wrap) => {
|
|
814
|
+
(key.revocationReason === "superseded" ? (_jsx("span", { className: "text-text-muted", children: " (superseded)" })) : (_jsx("span", { className: "text-danger", children: " (revoked)" })))] }, key.fingerprint))) })) : (_jsx("p", { className: "text-sm text-text-muted", children: "No keys enrolled yet." }))] }), _jsx(ZeroAccessSetting, { mailbox: mailbox, canManage: canManageKeys }), _jsx(KeyExportSetting, { mailbox: mailbox, canManage: canManageKeys }), _jsx(OpenPgpKeySetting, { mailbox: mailbox, canManage: canManageKeys }), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Digital signatures" }), activeSigningKey && signingStatus !== "pending" && (_jsx("p", { className: ["text-sm text-text-muted", certificateMode ? "mb-3" : ""].join(" "), children: "Enabled \u2014 outgoing mail from this mailbox is signed with a publicly-trusted certificate." })), certificateMode && (_jsxs("div", { className: "flex flex-col gap-2", children: [certificateMode === "failed" && signingError && _jsx(Alert, { children: signingError }), _jsx(SigningCertificateCard, { mode: certificateMode, enrollment: certificateEnrollment, snapshot: enrollment, info: signingInfo, address: mailbox.primarySmtpAddress, keyNotAfter: certificateMode === "expired" ? expiredSigningKey?.notAfter : activeSigningKey?.notAfter, canRequest: canManageKeys, requesting: signingStatus === "enrolling", onCheck: canManageKeys ? () => void checkEnrollmentNow(mailboxUid) : undefined, onRequest: handleEnrollSigning })] })), !activeSigningKey && !certificateMode && !canManageKeys ? (_jsx("p", { className: "text-sm text-text-muted", children: "Not enabled." })) : activeSigningKey || certificateMode ? null : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-xs text-text-muted", children: "Lets recipients verify that mail from this mailbox is genuinely from you. Optional \u2014 encryption already works without it." }), beforeRequestText(signingInfo, mailbox.primarySmtpAddress) && (_jsx("p", { className: "text-xs text-text-muted", children: beforeRequestText(signingInfo, mailbox.primarySmtpAddress) })), healthWarning(signingInfo) && _jsx(Alert, { children: healthWarning(signingInfo) }), enrollment?.gone && _jsx(Alert, { children: STALE_REQUEST_TEXT }), signingError && _jsx(Alert, { children: signingError }), signingInfo?.backend !== "none" && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: signingStatus === "enrolling", disabled: signingStatus !== "idle", onClick: handleEnrollSigning, children: "Enable digital signatures" }))] }))] }), mailbox.escrowScopeId && (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Escrow" }), escrowError && _jsx(Alert, { children: escrowError }), hasEscrowWrap ? (_jsx("p", { className: "text-sm text-text-muted", children: "This mailbox is under legal/compliance escrow \u2014 an authorized holder in your organization can recover its encrypted mail if needed. This does not weaken protection against anyone else." })) : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("p", { className: "text-xs text-text-muted", children: ["Your organization has assigned this mailbox to an escrow scope, but nothing has been protected yet \u2014 an authorized holder cannot recover this mailbox\u2019s encrypted mail until you complete this step.", " ", "This does not weaken protection against anyone else."] }), canManageKeys && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: wrappingEscrow, disabled: wrappingEscrow, onClick: handleWrapEscrow, children: "Add escrow protection" }))] })), _jsxs(Modal, { open: pendingEscrow !== null, onClose: () => setPendingEscrow(null), title: "Confirm escrow certificate", children: [_jsx("p", { className: "text-sm mb-3", children: "Anyone holding the private key for this certificate will be able to recover this mailbox\u2019s encrypted mail. Confirm it is the escrow certificate your organization expects before continuing." }), _jsxs("dl", { className: "text-sm mb-5 flex flex-col gap-1", children: [_jsxs("div", { children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Escrow scope" }), _jsx("dd", { className: "break-all", children: pendingEscrow?.escrowScopeId })] }), _jsxs("div", { children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Certificate fingerprint" }), _jsx("dd", { className: "font-mono break-all", children: pendingEscrow?.publicKey.fingerprint })] })] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setPendingEscrow(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => void handleConfirmEscrow(pendingEscrow), children: "Trust this certificate" })] })] })] })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Unlock methods" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["Removing a method here stops it from being usable to unlock this mailbox going forward, but it is ", _jsx("strong", { children: "not" }), " full revocation \u2014 anyone who already captured a wrapped copy and knows its secret could still use it. For real revocation (e.g. after a lost device), rotate your keys entirely below instead."] }), vault && vault.masterKeyWraps.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-2", children: [...passwordWraps, ...recoveryWraps, ...otherWraps].map((wrap) => {
|
|
803
815
|
const key = `${wrap.method}:${wrap.methodId ?? ""}`;
|
|
804
816
|
return (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsx("span", { children: METHOD_LABELS[wrap.method] ?? wrap.method }), wrap.method !== "escrow" &&
|
|
805
817
|
canManageKeys &&
|
|
@@ -7,7 +7,8 @@ import { pageTitle } from "../../../shared/navigation/pageTitle.js";
|
|
|
7
7
|
import { useEffect, useRef, useState } from "react";
|
|
8
8
|
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
9
9
|
import { createExportRequest, exportRequestDownloadUrl, listExportRequests, } from "../../../../lib/mail/dataExportApi.js";
|
|
10
|
-
import { listImportRequests, uploadMailboxImport, } from "../../../../lib/mail/mailboxImportApi.js";
|
|
10
|
+
import { ZERO_ACCESS_IMPORT_MESSAGE, listImportRequests, mailIsZeroAccess, uploadMailboxImport, } from "../../../../lib/mail/mailboxImportApi.js";
|
|
11
|
+
import { getEncryptionPolicy } from "../../../../lib/crypto/keyvaultApi.js";
|
|
11
12
|
import { createErasureRequest, listErasureRequests, } from "../../../../lib/mail/erasureRequestApi.js";
|
|
12
13
|
import { listFolders } from "../../../../lib/mail/mailApi.js";
|
|
13
14
|
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
@@ -16,6 +17,7 @@ import Alert from "../../../../lib/components/feedback/Alert.js";
|
|
|
16
17
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
17
18
|
import Modal from "../../../../lib/components/overlays/Modal.js";
|
|
18
19
|
import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
|
|
20
|
+
import { INVALID_REQUEST_CODE } from "../../../../lib/zeroAccess/sealedMoves.js";
|
|
19
21
|
import GravatarSetting from "../../../shared/components/settings/GravatarSetting.js";
|
|
20
22
|
const NON_MAIL_FOLDER_TYPES = new Set(["calendar", "contacts", "tasks", "notes"]);
|
|
21
23
|
function importFormatFromFilename(filename) {
|
|
@@ -50,7 +52,7 @@ function PrivacyContent({ userUid }) {
|
|
|
50
52
|
const ownedMailboxes = mailboxes.filter((mb) => mb.ownerUserUid !== undefined && mb.ownerUserUid === userUid);
|
|
51
53
|
const ownMailbox = ownedMailboxes.length === 1 ? ownedMailboxes[0] : undefined;
|
|
52
54
|
const selectedIsOwn = selected.uid === ownMailbox?.uid;
|
|
53
|
-
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl flex flex-col gap-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Privacy & Data" }), _jsx("p", { className: "text-sm text-text-muted", children: "Export or manage the data associated with this mailbox." })] }), _jsx(GravatarSetting, {}), selectedIsOwn ? (_jsxs(_Fragment, { children: [_jsx(ExportSection, { mailboxUid: mailboxUid, mailboxLabel: `${selected.displayName} (${selected.primarySmtpAddress})` }), _jsx(ImportSection, { mailboxUid: mailboxUid }), _jsx(ErasureSection, { mailboxLabel: `${selected.displayName} (${selected.primarySmtpAddress})` })] })) : (_jsx(OwnMailboxOnlyNotice, { ownedMailboxCount: ownedMailboxes.length, ownMailboxLabel: ownMailbox && `${ownMailbox.displayName} (${ownMailbox.primarySmtpAddress})` }))] }) }));
|
|
55
|
+
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl flex flex-col gap-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Privacy & Data" }), _jsx("p", { className: "text-sm text-text-muted", children: "Export or manage the data associated with this mailbox." })] }), _jsx(GravatarSetting, {}), selectedIsOwn ? (_jsxs(_Fragment, { children: [_jsx(ExportSection, { mailboxUid: mailboxUid, mailboxLabel: `${selected.displayName} (${selected.primarySmtpAddress})` }), _jsx(ImportSection, { mailboxUid: mailboxUid, mailbox: selected }), _jsx(ErasureSection, { mailboxLabel: `${selected.displayName} (${selected.primarySmtpAddress})` })] })) : (_jsx(OwnMailboxOnlyNotice, { ownedMailboxCount: ownedMailboxes.length, ownMailboxLabel: ownMailbox && `${ownMailbox.displayName} (${ownMailbox.primarySmtpAddress})` }))] }) }));
|
|
54
56
|
}
|
|
55
57
|
function OwnMailboxOnlyNotice({ ownedMailboxCount, ownMailboxLabel }) {
|
|
56
58
|
let detail;
|
|
@@ -108,10 +110,38 @@ function ExportSection({ mailboxUid, mailboxLabel }) {
|
|
|
108
110
|
setCreating(false);
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
|
-
return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Export my data" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["Requests a copy of ", _jsx("strong", { children: mailboxLabel }), "\u2019s messages, contacts, calendar, tasks, and notes. This runs in the background and can take a few minutes \u2014 check back here for a download link once it\u2019s ready."] }), _jsxs("form", { onSubmit: handleCreate, className: "flex items-end gap-3 mb-5", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Format" }), _jsxs("select", { "aria-label": "Export format", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: format, onChange: (e) => setFormat(e.target.value), children: [_jsx("option", { value: "json", children: "JSON" }), _jsx("option", { value: "mbox", children: "Mbox" })] })] }), _jsx(Button, { type: "submit", variant: "secondary", className: "!w-auto", loading: creating, disabled: creating, children: "Request export" })] }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : requests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No export requests yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: requests.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: [request.format.toUpperCase(), " \u00B7 ", 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) }), request.status === "ready" && (_jsx("a", { href: exportRequestDownloadUrl(request.uid), className: "text-primary-dark hover:underline font-medium", children: "Download" }))] })] }, request.uid))) }))] }));
|
|
113
|
+
return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Export my data" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["Requests a copy of ", _jsx("strong", { children: mailboxLabel }), "\u2019s messages, contacts, calendar, tasks, and notes. This runs in the background and can take a few minutes \u2014 check back here for a download link once it\u2019s ready. Mail stored with zero-access encryption is exported encrypted: open it in a mail app that has your exported keys (Settings \u203A Encryption)."] }), _jsxs("form", { onSubmit: handleCreate, className: "flex items-end gap-3 mb-5", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Format" }), _jsxs("select", { "aria-label": "Export format", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: format, onChange: (e) => setFormat(e.target.value), children: [_jsx("option", { value: "json", children: "JSON" }), _jsx("option", { value: "mbox", children: "Mbox" })] })] }), _jsx(Button, { type: "submit", variant: "secondary", className: "!w-auto", loading: creating, disabled: creating, children: "Request export" })] }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : requests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No export requests yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: requests.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: [request.format.toUpperCase(), " \u00B7 ", 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) }), request.status === "ready" && (_jsx("a", { href: exportRequestDownloadUrl(request.uid), className: "text-primary-dark hover:underline font-medium", children: "Download" }))] })] }, request.uid))) }))] }));
|
|
112
114
|
}
|
|
113
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Uploads a mail archive for restapi's server-side import. Not offered for a mailbox whose mail is zero-access (`mailIsZeroAccess()`):
|
|
117
|
+
* restapi refuses it (409), since the archive would wait readable in its blob store until the import ran, and the reason is shown
|
|
118
|
+
* 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
|
|
119
|
+
* 409, an import already in progress, is not.
|
|
120
|
+
*/
|
|
121
|
+
function ImportSection({ mailboxUid, mailbox }) {
|
|
114
122
|
const client = useApiClient();
|
|
123
|
+
// Only when the mailbox's own fields say its mail is zero-access is the administrator's policy asked: `undefined` until it answers
|
|
124
|
+
// (counted as allowed), and a failed read counts as allowed too, so the upload is never offered only to be refused.
|
|
125
|
+
const zeroAccessCandidate = mailIsZeroAccess(mailbox, undefined);
|
|
126
|
+
const [allowZeroAccess, setAllowZeroAccess] = useState(undefined);
|
|
127
|
+
const [refused, setRefused] = useState(null);
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (!zeroAccessCandidate) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
let cancelled = false;
|
|
133
|
+
void getEncryptionPolicy(client)
|
|
134
|
+
.then((policy) => {
|
|
135
|
+
if (!cancelled) {
|
|
136
|
+
setAllowZeroAccess(policy.allowZeroAccess);
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
.catch(() => undefined);
|
|
140
|
+
return () => {
|
|
141
|
+
cancelled = true;
|
|
142
|
+
};
|
|
143
|
+
}, [zeroAccessCandidate, client]);
|
|
144
|
+
const zeroAccess = mailIsZeroAccess(mailbox, allowZeroAccess);
|
|
115
145
|
const [folders, setFolders] = useState([]);
|
|
116
146
|
const [targetFolderUid, setTargetFolderUid] = useState("");
|
|
117
147
|
const [foldersError, setFoldersError] = useState(null);
|
|
@@ -161,10 +191,21 @@ function ImportSection({ mailboxUid }) {
|
|
|
161
191
|
setUploading(true);
|
|
162
192
|
uploadMailboxImport(file, { format: importFormatFromFilename(file.name), targetFolderUid, mailboxUid: mailboxUid }, client)
|
|
163
193
|
.then(() => loadRequests())
|
|
164
|
-
.catch((err) =>
|
|
194
|
+
.catch((err) => {
|
|
195
|
+
if (err instanceof ApiRequestError && err.status === 409 && err.code === INVALID_REQUEST_CODE) {
|
|
196
|
+
// The server refuses an import into this mailbox (its mail is zero-access): say why, in place of the upload.
|
|
197
|
+
setRefused(err.message);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// Any other refusal - an import already in progress is a 409 too - leaves the upload offered, and shows that import.
|
|
201
|
+
notifyApiError(err, "Couldn't upload the file");
|
|
202
|
+
if (err instanceof ApiRequestError && err.status === 409) {
|
|
203
|
+
void loadRequests();
|
|
204
|
+
}
|
|
205
|
+
})
|
|
165
206
|
.finally(() => setUploading(false));
|
|
166
207
|
}
|
|
167
|
-
return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Import mail" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Uploads an Mbox or PST archive and imports every message it contains into the folder you choose below. This runs in the background \u2014 check back here for its progress." }), folders.length > 0 && (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm mb-3 max-w-xs", children: [_jsx("span", { className: "font-semibold", children: "Destination folder" }), _jsx("select", { "aria-label": "Destination folder", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: targetFolderUid, onChange: (e) => setTargetFolderUid(e.target.value), children: folders.map((folder) => (_jsx("option", { value: folder.uid, children: folder.name }, folder.uid))) })] })), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto mb-3", loading: uploading, disabled: uploading || !targetFolderUid, onClick: () => fileInputRef.current?.click(), children: "Upload Mbox or PST file" }), _jsx("input", { ref: fileInputRef, type: "file", accept: ".mbox,.pst", "aria-label": "Upload mail archive", className: "hidden", onChange: handleFileChange }), foldersError && _jsx(Alert, { children: foldersError }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : requests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No import requests yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: requests.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: [request.format.toUpperCase(), " \u00B7 ", new Date(request.dateCreated).toLocaleString(), request.status === "completed" && (_jsxs("span", { className: "text-text-muted", children: [" ", "\u2014 ", request.importedCount ?? 0, " imported", request.failedCount ? `, ${request.failedCount} failed` : ""] })), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) })] }, request.uid))) }))] }));
|
|
208
|
+
return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Import mail" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Uploads an Mbox or PST archive and imports every message it contains into the folder you choose below. This runs in the background \u2014 check back here for its progress." }), zeroAccess || refused ? (_jsx(Alert, { children: refused ?? ZERO_ACCESS_IMPORT_MESSAGE })) : (_jsxs(_Fragment, { children: [folders.length > 0 && (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm mb-3 max-w-xs", children: [_jsx("span", { className: "font-semibold", children: "Destination folder" }), _jsx("select", { "aria-label": "Destination folder", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: targetFolderUid, onChange: (e) => setTargetFolderUid(e.target.value), children: folders.map((folder) => (_jsx("option", { value: folder.uid, children: folder.name }, folder.uid))) })] })), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto mb-3", loading: uploading, disabled: uploading || !targetFolderUid, onClick: () => fileInputRef.current?.click(), children: "Upload Mbox or PST file" }), _jsx("input", { ref: fileInputRef, type: "file", accept: ".mbox,.pst", "aria-label": "Upload mail archive", className: "hidden", onChange: handleFileChange })] })), foldersError && _jsx(Alert, { children: foldersError }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : requests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No import requests yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: requests.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: [request.format.toUpperCase(), " \u00B7 ", new Date(request.dateCreated).toLocaleString(), request.status === "completed" && (_jsxs("span", { className: "text-text-muted", children: [" ", "\u2014 ", request.importedCount ?? 0, " imported", request.failedCount ? `, ${request.failedCount} failed` : ""] })), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) })] }, request.uid))) }))] }));
|
|
168
209
|
}
|
|
169
210
|
function ErasureSection({ mailboxLabel }) {
|
|
170
211
|
const client = useApiClient();
|
|
@@ -26,6 +26,9 @@ import { useShortcut } from "../../shared/keyboard/useShortcut.js";
|
|
|
26
26
|
import { useShortcutProps } from "../../shared/keyboard/useShortcutProps.js";
|
|
27
27
|
import { notifyApiError } from "../../shared/notifications/apiErrors.js";
|
|
28
28
|
import { changedItemOf, useFolderLiveUpdates } from "../../shared/live/useFolderLiveUpdates.js";
|
|
29
|
+
import ProtectedMark from "../../shared/zeroAccess/ProtectedMark.js";
|
|
30
|
+
import { useKeysLocked } from "../../shared/zeroAccess/useKeysLocked.js";
|
|
31
|
+
import { isProtectedRecord, openSealedRecord, openSealedRecords } from "../../../lib/zeroAccess/sealedRecords.js";
|
|
29
32
|
/** The published model class name of a task on either database (`TaskMongo`, `TaskSQL`) - not a `TaskList`'s. */
|
|
30
33
|
const TASK_MODEL = /^Task(Mongo|SQL)$/;
|
|
31
34
|
/** The push client channel group (`PushClient.setChannels()`) the page's tasks folder is subscribed under. */
|
|
@@ -106,6 +109,7 @@ function TasksContent() {
|
|
|
106
109
|
setLoading(true);
|
|
107
110
|
setError(null);
|
|
108
111
|
return listAllPages((page) => listTasks(folderUid, { limit: LIST_PAGE_SIZE, page }, client))
|
|
112
|
+
.then(async (result) => ({ ...result, items: await openSealedRecords(result.items) }))
|
|
109
113
|
.then((result) => {
|
|
110
114
|
if (run === loadRun.current) {
|
|
111
115
|
setTasks(result.items);
|
|
@@ -126,6 +130,14 @@ function TasksContent() {
|
|
|
126
130
|
useEffect(() => {
|
|
127
131
|
void reload();
|
|
128
132
|
}, [folderUid]);
|
|
133
|
+
// The list holds protected tasks opened with the mailbox's keys: its lock takes them off screen, read again with their placeholders. They
|
|
134
|
+
// leave the list first, so a reload that fails can't leave them shown.
|
|
135
|
+
useKeysLocked((locked) => {
|
|
136
|
+
if (locked === mailboxUid) {
|
|
137
|
+
setTasks((list) => list.filter((task) => !isProtectedRecord(task)));
|
|
138
|
+
void reload();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
129
141
|
// 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
|
|
130
142
|
// 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
|
|
131
143
|
// failed fetch changes nothing: the next load shows it as it is.
|
|
@@ -135,7 +147,7 @@ function TasksContent() {
|
|
|
135
147
|
remove();
|
|
136
148
|
return;
|
|
137
149
|
}
|
|
138
|
-
void getTask(change.uid, client).then((fresh) => {
|
|
150
|
+
void getTask(change.uid, client).then(openSealedRecord).then((fresh) => {
|
|
139
151
|
if (fresh.folderUid !== folderUid) {
|
|
140
152
|
remove();
|
|
141
153
|
return;
|
|
@@ -199,14 +211,14 @@ function TasksContent() {
|
|
|
199
211
|
setCreateError("That mailbox has no Tasks folder.");
|
|
200
212
|
return;
|
|
201
213
|
}
|
|
202
|
-
const created = await createTask({
|
|
214
|
+
const created = await openSealedRecord(await createTask({
|
|
203
215
|
mailboxUid: target,
|
|
204
216
|
folderUid: targetFolderUid,
|
|
205
217
|
title: title.trim(),
|
|
206
218
|
// The date input's "yyyy-MM-dd" is stored as that day's UTC midnight - see `dueDateInstant()`.
|
|
207
219
|
dueDate: dueDate ? dueDateInstant(dueDate) : undefined,
|
|
208
220
|
priority,
|
|
209
|
-
}, client);
|
|
221
|
+
}, client));
|
|
210
222
|
if (target === mailboxUid) {
|
|
211
223
|
setAddedElsewhere(null);
|
|
212
224
|
setTasks((prev) => [...prev, created]);
|
|
@@ -247,11 +259,13 @@ function TasksContent() {
|
|
|
247
259
|
const nextCompleted = !task.completed;
|
|
248
260
|
setTasks((prev) => prev.map((t) => (t.uid === task.uid ? { ...t, completed: nextCompleted } : t)));
|
|
249
261
|
try {
|
|
250
|
-
const updated = await setTaskCompleted(task, nextCompleted, client);
|
|
262
|
+
const updated = await openSealedRecord(await setTaskCompleted(task, nextCompleted, client));
|
|
251
263
|
setTasks((prev) => prev.map((t) => (t.uid === task.uid ? updated : t)));
|
|
252
264
|
}
|
|
253
265
|
catch (err) {
|
|
254
|
-
|
|
266
|
+
// 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
|
|
267
|
+
// reloaded it with its placeholders (#224), which putting it back whole would undo.
|
|
268
|
+
setTasks((prev) => prev.map((t) => (t.uid === task.uid ? { ...t, completed: task.completed } : t)));
|
|
255
269
|
notifyApiError(err, "Couldn't update the task");
|
|
256
270
|
}
|
|
257
271
|
finally {
|
|
@@ -415,7 +429,7 @@ function CompletionToggle({ task, onToggle, pending }) {
|
|
|
415
429
|
].join(" "), children: "\u2713" }));
|
|
416
430
|
}
|
|
417
431
|
function TaskGroup({ label, tasks, checkedUids, onToggleChecked, onToggle, toggling, onDelete }) {
|
|
418
|
-
return (_jsxs("div", { children: [_jsx("h2", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-2", children: label }), _jsx("ul", { className: "bg-surface border border-border rounded-md divide-y divide-border", children: tasks.map((task) => (_jsxs("li", { className: "flex items-center gap-3 py-2.5 px-3", children: [_jsx("input", { type: "checkbox", "aria-label": `Select ${task.title}`, checked: checkedUids.has(task.uid), onChange: () => onToggleChecked(task.uid) }), _jsx(CompletionToggle, { task: task, onToggle: onToggle, pending: toggling.has(task.uid) }),
|
|
432
|
+
return (_jsxs("div", { children: [_jsx("h2", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-2", children: label }), _jsx("ul", { className: "bg-surface border border-border rounded-md divide-y divide-border", children: tasks.map((task) => (_jsxs("li", { className: "flex items-center gap-3 py-2.5 px-3", children: [_jsx("input", { type: "checkbox", "aria-label": `Select ${task.title}`, checked: checkedUids.has(task.uid), onChange: () => onToggleChecked(task.uid) }), _jsx(CompletionToggle, { task: task, onToggle: onToggle, pending: toggling.has(task.uid) }), _jsxs("span", { className: ["flex-1 text-sm", task.completed ? "line-through text-text-muted" : ""].join(" "), children: [task.title, _jsx(ProtectedMark, { record: task })] }), task.priority !== "normal" && (_jsx("span", { className: ["text-xs font-medium", PRIORITY_CLASS[task.priority]].join(" "), children: PRIORITY_LABEL[task.priority] })), task.dueDate && (_jsx("span", { className: "text-xs text-text-muted shrink-0", children: parseDueDate(task.dueDate).toLocaleDateString() })), _jsx("button", { type: "button", onClick: () => onDelete(task), "aria-label": `Delete "${task.title}"`, className: "text-text-muted hover:text-danger text-sm px-1", children: "\u00D7" })] }, task.uid))) })] }));
|
|
419
433
|
}
|
|
420
434
|
function TaskTable({ tasks, checkedUids, onToggleChecked, onToggle, toggling, onDelete }) {
|
|
421
435
|
const allChecked = tasks.length > 0 && tasks.every((t) => checkedUids.has(t.uid));
|
|
@@ -435,7 +449,7 @@ function TaskTable({ tasks, checkedUids, onToggleChecked, onToggle, toggling, on
|
|
|
435
449
|
}
|
|
436
450
|
}
|
|
437
451
|
}
|
|
438
|
-
return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm bg-surface border border-border rounded-md", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [_jsx("th", { className: "w-8 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all tasks", checked: allChecked, onChange: toggleAll }) }), _jsx("th", { className: "w-8 px-3 py-2" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Title" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Due Date" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Importance" }), _jsx("th", { className: "w-8 px-3 py-2" })] }) }), _jsx("tbody", { children: tasks.map((task) => (_jsxs("tr", { className: "border-b border-border last:border-0", children: [_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${task.title}`, checked: checkedUids.has(task.uid), onChange: () => onToggleChecked(task.uid) }) }), _jsx("td", { className: "px-3 py-2", children: _jsx(CompletionToggle, { task: task, onToggle: onToggle, pending: toggling.has(task.uid) }) }),
|
|
452
|
+
return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm bg-surface border border-border rounded-md", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [_jsx("th", { className: "w-8 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all tasks", checked: allChecked, onChange: toggleAll }) }), _jsx("th", { className: "w-8 px-3 py-2" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Title" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Due Date" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Importance" }), _jsx("th", { className: "w-8 px-3 py-2" })] }) }), _jsx("tbody", { children: tasks.map((task) => (_jsxs("tr", { className: "border-b border-border last:border-0", children: [_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${task.title}`, checked: checkedUids.has(task.uid), onChange: () => onToggleChecked(task.uid) }) }), _jsx("td", { className: "px-3 py-2", children: _jsx(CompletionToggle, { task: task, onToggle: onToggle, pending: toggling.has(task.uid) }) }), _jsxs("td", { className: ["px-3 py-2", task.completed ? "line-through text-text-muted" : ""].join(" "), children: [task.title, _jsx(ProtectedMark, { record: task })] }), _jsx("td", { className: "px-3 py-2 text-text-muted", children: task.dueDate ? parseDueDate(task.dueDate).toLocaleDateString() : "" }), _jsx("td", { className: ["px-3 py-2", PRIORITY_CLASS[task.priority]].join(" "), children: PRIORITY_LABEL[task.priority] }), _jsx("td", { className: "px-3 py-2", children: _jsx("button", { type: "button", onClick: () => onDelete(task), "aria-label": `Delete "${task.title}"`, className: "text-text-muted hover:text-danger text-sm px-1", children: "\u00D7" }) })] }, task.uid))) })] }) }));
|
|
439
453
|
}
|
|
440
454
|
export default TasksPage;
|
|
441
455
|
/** The tab's title: `Brand: Tasks` (see `pageTitle()`). */
|
|
@@ -56,7 +56,7 @@ export declare function deleteDomain(uid: string, version: number, client?: ApiC
|
|
|
56
56
|
* Idempotent on an already-verified domain; a still-unverified result is not an error (only
|
|
57
57
|
* `lastCheckedAt` advances) — always returns the domain's current state either way. */
|
|
58
58
|
export declare function verifyDomain(uid: string, client?: ApiClient): Promise<Domain>;
|
|
59
|
-
export type DnsRecordType = "ownership" | "mx" | "spf" | "dkim" | "dmarc" | "autodiscover_cname" | "autodiscover_srv";
|
|
59
|
+
export type DnsRecordType = "ownership" | "mx" | "spf" | "dkim" | "dmarc" | "openpgpkey_cname" | "autodiscover_cname" | "autodiscover_srv";
|
|
60
60
|
/** One mail-related DNS record this server recommends for a domain, live-checked against real DNS —
|
|
61
61
|
* purely diagnostic, never mutates anything (unlike `verifyDomain`). The two `autodiscover_*` types are only
|
|
62
62
|
* ever present while `@rapidmx/autodiscover-plugin` is active for this deployment. */
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { ApiClient } from "../util/api.js";
|
|
11
11
|
import { ListParams, RequestListParams } from "../util/apiQuery.js";
|
|
12
|
+
import type { PublicKey, WrappedPrivateKey } from "../crypto/keyvaultApi.js";
|
|
12
13
|
export type { ListParams, RequestListParams };
|
|
13
14
|
export interface EscrowAccessRequestApproval {
|
|
14
15
|
holderUserUid: string;
|
|
@@ -54,6 +55,11 @@ export interface MasterKeyWrap {
|
|
|
54
55
|
* tooling, per `specs/end-to-end_encryption.md`'s "this server never holds the scope's private key". */
|
|
55
56
|
export interface EscrowAccessMaterial {
|
|
56
57
|
masterKeyWraps: MasterKeyWrap[];
|
|
58
|
+
/** The mailbox's wrapped private keys that read mail (zero-access and encryption keys, never signing), sealed under its master key. Absent
|
|
59
|
+
* from a server that predates releasing them. */
|
|
60
|
+
wrappedKeys?: WrappedPrivateKey[];
|
|
61
|
+
/** The certificate of each of `wrappedKeys`, matched by fingerprint. */
|
|
62
|
+
certificates?: PublicKey[];
|
|
57
63
|
}
|
|
58
64
|
/** `client`, given by every function below, is an explicit `ApiClient` from `createApiClient()` (e.g. one
|
|
59
65
|
* account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* header comment for the shared ACL/authorization model every wrapper file here follows. Recurrence
|
|
3
3
|
* expansion happens entirely client-side (see `apps/shared/lib/recurrence.ts`) — the backend stores/returns
|
|
4
4
|
* `RecurrenceRule` as-is and does no RFC5545 expansion of its own. */
|
|
5
|
-
import { ApiClient } from "../util/api.js";
|
|
5
|
+
import { ApiClient, ApiRequestError } from "../util/api.js";
|
|
6
|
+
import { type InvitationContent } from "./protectedEvents.js";
|
|
6
7
|
export type AttendeeRole = "required" | "optional" | "resource";
|
|
7
8
|
export type AttendeeResponseStatus = "needsAction" | "accepted" | "declined" | "tentative";
|
|
8
9
|
export interface Attendee {
|
|
@@ -112,6 +113,16 @@ export interface CalendarEvent {
|
|
|
112
113
|
* `mailto` only). Still to be rendered through `sanitizeEventDescriptionHtml()` (`eventDescription.ts`), never trusted as it stands. `null` or absent:
|
|
113
114
|
* the description, if any, is plain text. */
|
|
114
115
|
descriptionHtml?: string | null;
|
|
116
|
+
/** Response-only. An event made from an invitation that reached a zero-access mailbox: its title, location and description are only
|
|
117
|
+
* sealed to the mailbox's zero-access key (`title` is `[...]`). `apps/shared/calendar/sealedEvents.ts` opens them. */
|
|
118
|
+
sealedContent?: {
|
|
119
|
+
keyFingerprint: string;
|
|
120
|
+
sealed: string;
|
|
121
|
+
};
|
|
122
|
+
/** Response-only. An event of a calendar zero-access encryption protects: its title, location and description, each sealed to the
|
|
123
|
+
* mailbox's zero-access key (the readable `title` is "Protected event"). Every function here returns events with them opened when the
|
|
124
|
+
* mailbox is unlocked (`lib/zeroAccess/sealedRecords.ts`); writes go through `protectedEvents.ts`. */
|
|
125
|
+
sealedFields?: Record<string, string> | null;
|
|
115
126
|
/** Who may see the event's details - absent (or `null`, on a row stored before this existed) reads as `"default"`; see `visibilityOf()`. */
|
|
116
127
|
visibility?: EventVisibility | null;
|
|
117
128
|
/** Whether the guests may ask for the event to change (default `false`). Absent or `null` reads as the default; see `guestPermissionsOf()`. */
|
|
@@ -141,6 +152,14 @@ export interface CalendarEvent {
|
|
|
141
152
|
* `LIST_CALENDAR_EVENTS_MAX_PAGES` pages guards against looping forever should a server ever ignore
|
|
142
153
|
* `page` and keep returning full pages.
|
|
143
154
|
*/
|
|
155
|
+
/** What a list opens of a protected event: what the calendar views draw. Each field is one decryption, and a calendar lists every event
|
|
156
|
+
* it has - the rest (the description) is opened by the view that shows or edits it, by a search, and by a save. */
|
|
157
|
+
export declare const EVENT_LIST_FIELDS: readonly ["title", "location"];
|
|
158
|
+
/** `event` with every protected field opened, when only some were (a listed event) - for a write, which sends only what changed and
|
|
159
|
+
* invites with the whole content. The title of one whose `sealedContent` was opened stays: a row can hold both, and its per-field copy
|
|
160
|
+
* (`sealedFields.title`) is then an older title the sealed content's replaced (renamed since) - or, sealed without one, the same title
|
|
161
|
+
* `openSealedEvents()` already shows. */
|
|
162
|
+
export declare function fullyOpenedEvent<T extends CalendarEvent | undefined>(event: T): Promise<T>;
|
|
144
163
|
export declare function listCalendarEvents(folderUid: string, client?: ApiClient): Promise<CalendarEvent[]>;
|
|
145
164
|
export declare function getCalendarEvent(uid: string, client?: ApiClient): Promise<CalendarEvent>;
|
|
146
165
|
export interface CalendarEventInput {
|
|
@@ -172,13 +191,38 @@ export interface CalendarEventInput {
|
|
|
172
191
|
guestsCanModify?: boolean;
|
|
173
192
|
guestsCanInviteOthers?: boolean;
|
|
174
193
|
guestsCanSeeGuestList?: boolean;
|
|
194
|
+
/**
|
|
195
|
+
* What the server sends a protected event's save-time invitations with (restapi `BaseCalendarEventRoute.takeInvitationContent()`).
|
|
196
|
+
* On a create, `{ defer: true }` holds them back for the save that follows - the event editor's, which attaches the video meeting and
|
|
197
|
+
* its link keys first - so attendees get one invitation, the one with the link. On an update, `{ defer: true }` sends none with this
|
|
198
|
+
* save either (they stay pending), and a content (`title`...) is what `updateCalendarEvent()` invites with when `current` couldn't be
|
|
199
|
+
* opened on this device - the content it opened wins otherwise.
|
|
200
|
+
*/
|
|
201
|
+
invitationContent?: Partial<InvitationContent> & {
|
|
202
|
+
defer?: boolean;
|
|
203
|
+
};
|
|
175
204
|
}
|
|
176
205
|
export declare function createCalendarEvent(input: CalendarEventInput, client?: ApiClient): Promise<CalendarEvent>;
|
|
177
206
|
export interface UpdateCalendarEventInput extends Partial<CalendarEventInput> {
|
|
178
207
|
uid: string;
|
|
179
208
|
version: number;
|
|
180
209
|
}
|
|
181
|
-
|
|
210
|
+
/** Saves `input`. `current` is the event as this device shows it: for an event of a protected calendar, the write then sends only the
|
|
211
|
+
* content that changed, and the content the server invites with (`protectedEventWrite()`; `input.invitationContent` when `current` isn't
|
|
212
|
+
* opened, none with `{ defer: true }`). For an event whose video meeting is protected,
|
|
213
|
+
* every attendee's link key goes with it (`meetingLinkSecrets()`), for the server to add to their links in the invitations it sends. */
|
|
214
|
+
export declare function updateCalendarEvent(input: UpdateCalendarEventInput, client?: ApiClient, current?: CalendarEvent): Promise<CalendarEvent>;
|
|
215
|
+
/** What `SealedEventContentError` says. */
|
|
216
|
+
export declare const SEALED_EVENT_CONTENT_MESSAGE = "This event came from an invitation to a zero-access mailbox and its details are sealed, so its location or description can only be changed together with its title. Unlock your mailbox, open the event again and make the change there.";
|
|
217
|
+
/**
|
|
218
|
+
* restapi's refusal (409) of a location or description change to an event made from a zero-access invitation (`sealedContent`) sent
|
|
219
|
+
* with its placeholder title or none: resealing would lose the title. `saveCalendarEvent()` leaves out the placeholders of one it hasn't
|
|
220
|
+
* opened, so this comes back only when this device didn't know the event was sealed (it was filed sealed after it was loaded). An
|
|
221
|
+
* `ApiRequestError` (409), as `SealedMoveError` is, so the event dialog and the drag handler show its message.
|
|
222
|
+
*/
|
|
223
|
+
export declare class SealedEventContentError extends ApiRequestError {
|
|
224
|
+
constructor();
|
|
225
|
+
}
|
|
182
226
|
export declare function deleteCalendarEvent(uid: string, version: number, client?: ApiClient): Promise<void>;
|
|
183
227
|
/** The subset of `AttendeeResponseStatus` a caller can actually respond with — `"needsAction"` is only
|
|
184
228
|
* ever a default/initial value, never a valid response. */
|
|
@@ -190,9 +234,10 @@ export type AttendeeResponseInput = Exclude<AttendeeResponseStatus, "needsAction
|
|
|
190
234
|
* `"accepted"`/`"tentative"` the whole updated `CalendarEvent` comes back, with only the calling
|
|
191
235
|
* attendee's own `responseStatus` changed. On `"declined"`, the backend instead soft-deletes the
|
|
192
236
|
* mailbox's own copy of the event and returns only `{ uid }` — the caller should treat a decline the
|
|
193
|
-
* same as a delete rather than expecting an updated event back.
|
|
237
|
+
* same as a delete rather than expecting an updated event back. `current` (the event as this device shows it) names a protected event's
|
|
238
|
+
* reply by its real title (`invitationContentOf()`).
|
|
194
239
|
*/
|
|
195
|
-
export declare function respondToEvent(uid: string, responseStatus: AttendeeResponseInput, client?: ApiClient): Promise<CalendarEvent | {
|
|
240
|
+
export declare function respondToEvent(uid: string, responseStatus: AttendeeResponseInput, client?: ApiClient, current?: CalendarEvent): Promise<CalendarEvent | {
|
|
196
241
|
uid: string;
|
|
197
242
|
}>;
|
|
198
243
|
/** A change a guest asks the organizer for (`requestEventChange()`): only what is to differ. */
|
|
@@ -225,6 +270,7 @@ export interface EventChangeRequestResult {
|
|
|
225
270
|
* Asks the organizer to change an event, as a guest (not the organizer) whose copy allows it: the title, location, description or time need
|
|
226
271
|
* `guestsCanModify`, added guests need `guestsCanInviteOthers` (403 otherwise; 400 when nothing differs from what the event has, or a value is invalid).
|
|
227
272
|
* Nothing changes on this mailbox's own copy - the request is mailed to the organizer, and when the change is allowed it is applied there and
|
|
228
|
-
* arrives back later as an ordinary update of the invitation.
|
|
273
|
+
* arrives back later as an ordinary update of the invitation. `invitationContent` (the event's real content, `invitationContentOf()`)
|
|
274
|
+
* names a protected event in the request by its real title.
|
|
229
275
|
*/
|
|
230
|
-
export declare function requestEventChange(uid: string, request: EventChangeRequest, client?: ApiClient): Promise<EventChangeRequestResult>;
|
|
276
|
+
export declare function requestEventChange(uid: string, request: EventChangeRequest, client?: ApiClient, invitationContent?: InvitationContent): Promise<EventChangeRequestResult>;
|