@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
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import { useCallback, useEffect, useReducer } from "react";
|
|
6
6
|
import { getMessageInvite } from "../../../../../lib/calendar/inviteApi.js";
|
|
7
|
+
import { clearSealedInvites, onSealedInvitesForgotten, sealedInviteCalendar } from "../../../../../lib/calendar/sealedInvites.js";
|
|
8
|
+
import { onSigningOut } from "../compose/composeFlushRegistry.js";
|
|
7
9
|
import { useApiClient } from "../../../../../lib/util/apiClientContext.js";
|
|
8
10
|
/**
|
|
9
11
|
* A per-message cache of the calendar invitation the server reads out of a message, shared by everything that draws one: the reading pane's
|
|
@@ -37,7 +39,9 @@ function normalize(invite) {
|
|
|
37
39
|
*/
|
|
38
40
|
export function loadInvite(uid, client) {
|
|
39
41
|
const existing = entries.get(uid);
|
|
40
|
-
|
|
42
|
+
// "No invitation" for a zero-access message is asked again once its calendar file has been decrypted here (the list asks before that).
|
|
43
|
+
const stale = existing?.value === null && sealedInviteCalendar(uid) !== undefined;
|
|
44
|
+
if (existing && !stale && (existing.value === undefined || Date.now() - existing.at < INVITE_TTL_MS)) {
|
|
41
45
|
return existing.promise;
|
|
42
46
|
}
|
|
43
47
|
const entry = {
|
|
@@ -100,3 +104,14 @@ export function useMessageInvite(uid, enabled = true) {
|
|
|
100
104
|
const value = enabled ? entries.get(uid)?.value : undefined;
|
|
101
105
|
return { invite: value ?? null, settled: value !== undefined, setInvite };
|
|
102
106
|
}
|
|
107
|
+
onSigningOut(clearSealedInvites);
|
|
108
|
+
onSigningOut(clearInviteCache);
|
|
109
|
+
// What the server read from a zero-access message's decrypted calendar file (its summary, description) goes with the file when the mailbox
|
|
110
|
+
// locks: mounted cards, chips and popovers draw nothing until it is read again.
|
|
111
|
+
onSealedInvitesForgotten((uids) => {
|
|
112
|
+
for (const uid of uids) {
|
|
113
|
+
if (entries.delete(uid)) {
|
|
114
|
+
notify(uid);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
@@ -15,6 +15,7 @@ import KeyEnrollmentGate from "../../layout/KeyEnrollmentGate.js";
|
|
|
15
15
|
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
16
16
|
import { prefetchComposeWindow, useCompose } from "../compose/ComposeContext.js";
|
|
17
17
|
import LocalIndexLifecycle from "../../../search/LocalIndexLifecycle.js";
|
|
18
|
+
import SharedMailboxKeyManager from "../../layout/SharedMailboxKeyManager.js";
|
|
18
19
|
import { NO_LIVE_UPDATES } from "../../../mail/useMailLiveUpdates.js";
|
|
19
20
|
import { MAILBOX_LIST_LIMIT, MailConnectionContext, useMailConnection } from "../../../mail/useMailConnection.js";
|
|
20
21
|
import { folderRows } from "../../../mail/folderTree.js";
|
|
@@ -264,7 +265,7 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
|
|
|
264
265
|
// A shared/delegated mailbox the caller doesn't own is never provisioned from here - its keys
|
|
265
266
|
// belong to its owner (or its admins), not to whoever happens to open it first - and neither is
|
|
266
267
|
// the impersonated user's own mailbox: an administrator must never choose its password/recovery codes.
|
|
267
|
-
canProvision: !impersonating && activeMailbox?.ownerUserUid === userUid, children: [_jsx(LocalIndexLifecycle, { mailboxUid: activeMailboxUid, mailboxKeys: activeMailbox?.keys, folders: mailboxFolders.find((mf) => mf.mailbox.uid === activeMailboxUid)?.folders ?? [],
|
|
268
|
+
canProvision: !impersonating && activeMailbox?.ownerUserUid === userUid, children: [_jsx(SharedMailboxKeyManager, { mailboxes: mailboxes, userUid: userUid }), _jsx(LocalIndexLifecycle, { mailboxUid: activeMailboxUid, mailboxKeys: activeMailbox?.keys, folders: mailboxFolders.find((mf) => mf.mailbox.uid === activeMailboxUid)?.folders ?? [],
|
|
268
269
|
// A full page may be truncated - pruning against it would delete indexes of accessible mailboxes
|
|
269
270
|
// beyond the limit, so it's only trusted as the complete list when it came back short.
|
|
270
271
|
accessibleMailboxUids: status === "ready" && mailboxes.length < MAILBOX_LIST_LIMIT ? mailboxes.map((mb) => mb.uid) : undefined }), _jsx(AppShell, { active: "mail", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner })] }));
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { SignerKeyState } from "../../../../lib/contacts/contactsApi.js";
|
|
22
22
|
import type { ApiClient } from "../../../../lib/util/api.js";
|
|
23
|
+
import type { SignerPins } from "../../../../lib/crypto/messageSecurity.js";
|
|
23
24
|
/** How long one mailbox's loaded contact list is reused before it is fetched again. */
|
|
24
25
|
export declare const CONTACTS_CACHE_TTL_MS = 60000;
|
|
25
26
|
/** Forgets every cached contact list for the default global path - on sign-out, a key-session lock, after the
|
|
@@ -28,12 +29,17 @@ export declare const CONTACTS_CACHE_TTL_MS = 60000;
|
|
|
28
29
|
export declare function clearPinnedSignerCache(): void;
|
|
29
30
|
/** The sender `address`'s pinned signing fingerprints from `mailboxUid`'s contacts. Rejects if they can't be loaded. */
|
|
30
31
|
export declare function getPinnedSignerFingerprints(mailboxUid: string, address: string, client?: ApiClient): Promise<string[]>;
|
|
32
|
+
/** The sender `address`'s pinned signing fingerprints and pinned OpenPGP certificates from `mailboxUid`'s contacts - what
|
|
33
|
+
* `evaluateMessageSecurity()` takes as `SignerPins`. Rejects if they can't be loaded. */
|
|
34
|
+
export declare function getPinnedSigners(mailboxUid: string, address: string, client?: ApiClient): Promise<SignerPins>;
|
|
31
35
|
/** A sender's signing-key state (`signerKeyStateFor()`) plus what a key-changed notice needs to show and link. */
|
|
32
36
|
export interface SenderKeyState extends SignerKeyState {
|
|
33
37
|
/** The contact holding the first pinned signing key, else the one holding the conflict, else the first match. */
|
|
34
38
|
contactUid?: string;
|
|
35
39
|
/** When the first pinned signing key started being trusted - see `keyPinnedSince()`. */
|
|
36
40
|
pinnedSince?: number;
|
|
41
|
+
/** When the first pinned OpenPGP certificate (`openPgp[0]`) started being trusted. */
|
|
42
|
+
openPgpPinnedSince?: number;
|
|
37
43
|
}
|
|
38
44
|
/** The sender `address`'s signing-key state from `mailboxUid`'s (cached) contacts, for a "signing key changed" comparison
|
|
39
45
|
* or to spot a recorded conflict. Reads stored contacts only. Rejects if they can't be loaded. */
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* `tauri-client` accounts must never answer each other's "who does this sender's key belong to" question from
|
|
23
23
|
* one shared cache keyed only by `mailboxUid` (mailbox uids are not guaranteed unique across accounts/servers).
|
|
24
24
|
*/
|
|
25
|
-
import { PINNED_FINGERPRINT_MAX_PAGES, PINNED_FINGERPRINT_PAGE_SIZE, listContacts, pinnedSigningFingerprintsFor, signerKeyStateFor, } from "../../../../lib/contacts/contactsApi.js";
|
|
25
|
+
import { PINNED_FINGERPRINT_MAX_PAGES, PINNED_FINGERPRINT_PAGE_SIZE, listContacts, pinnedOpenPgpKeysFor, pinnedSigningFingerprintsFor, signerKeyStateFor, } from "../../../../lib/contacts/contactsApi.js";
|
|
26
26
|
import { keyPinnedSince } from "../contacts/contactKeys.js";
|
|
27
27
|
import { listFolders } from "../../../../lib/mail/mailApi.js";
|
|
28
28
|
import { subscribeKeySession } from "../../../../lib/crypto/keySession.js";
|
|
@@ -96,6 +96,12 @@ function cachedMailboxContacts(mailboxUid, client) {
|
|
|
96
96
|
export async function getPinnedSignerFingerprints(mailboxUid, address, client) {
|
|
97
97
|
return pinnedSigningFingerprintsFor(await cachedMailboxContacts(mailboxUid, client), address);
|
|
98
98
|
}
|
|
99
|
+
/** The sender `address`'s pinned signing fingerprints and pinned OpenPGP certificates from `mailboxUid`'s contacts - what
|
|
100
|
+
* `evaluateMessageSecurity()` takes as `SignerPins`. Rejects if they can't be loaded. */
|
|
101
|
+
export async function getPinnedSigners(mailboxUid, address, client) {
|
|
102
|
+
const contacts = await cachedMailboxContacts(mailboxUid, client);
|
|
103
|
+
return { fingerprints: pinnedSigningFingerprintsFor(contacts, address), openPgpKeys: pinnedOpenPgpKeysFor(contacts, address) };
|
|
104
|
+
}
|
|
99
105
|
/** The sender `address`'s signing-key state from `mailboxUid`'s (cached) contacts, for a "signing key changed" comparison
|
|
100
106
|
* or to spot a recorded conflict. Reads stored contacts only. Rejects if they can't be loaded. */
|
|
101
107
|
export async function getSignerKeyState(mailboxUid, address, client) {
|
|
@@ -109,9 +115,12 @@ export async function getSignerKeyState(mailboxUid, address, client) {
|
|
|
109
115
|
const contact = holder ??
|
|
110
116
|
matching.find((candidate) => (candidate.keyConflicts ?? []).some((conflict) => conflict.useType === "sign")) ??
|
|
111
117
|
matching[0];
|
|
118
|
+
const openPgpKey = state.openPgp?.[0];
|
|
119
|
+
const openPgpHolder = openPgpKey && matching.find((candidate) => (candidate.keys ?? []).includes(openPgpKey));
|
|
112
120
|
return {
|
|
113
121
|
...state,
|
|
114
122
|
...(contact ? { contactUid: contact.uid } : {}),
|
|
115
123
|
...(holder ? { pinnedSince: keyPinnedSince(holder, pinned) } : {}),
|
|
124
|
+
...(openPgpHolder ? { openPgpPinnedSince: keyPinnedSince(openPgpHolder, openPgpKey) } : {}),
|
|
116
125
|
};
|
|
117
126
|
}
|
|
@@ -9,8 +9,8 @@ export declare function printHeaders(message: Message, from: string): PrintableM
|
|
|
9
9
|
*/
|
|
10
10
|
export declare function olderConversationMessages(message: Message, client?: ApiClient): Promise<Message[]>;
|
|
11
11
|
/**
|
|
12
|
-
* One earlier message of a conversation as it prints: the body the reading pane would show. An encrypted one
|
|
13
|
-
* its mailbox's keys unlocked; when it can't be, it prints a line saying so rather than the ciphertext. `client` is the explicit `ApiClient` every
|
|
12
|
+
* One earlier message of a conversation as it prints: the body the reading pane would show. An encrypted one - end to end, or sealed to its mailbox's
|
|
13
|
+
* zero-access key, whose subject and body the server doesn't hold readable - is decrypted here when this device has its mailbox's keys unlocked; when it can't be, it prints a line saying so rather than the ciphertext. `client` is the explicit `ApiClient` every
|
|
14
14
|
* request goes through when given (see `useApiClient()`); without one they are the cookie requests they always were.
|
|
15
15
|
*/
|
|
16
16
|
export declare function printableOlderMessage(message: Message, client?: ApiClient): Promise<PrintableMessage>;
|
|
@@ -33,13 +33,13 @@ export async function olderConversationMessages(message, client) {
|
|
|
33
33
|
.sort((a, b) => new Date(b.receivedDate).getTime() - new Date(a.receivedDate).getTime());
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
* One earlier message of a conversation as it prints: the body the reading pane would show. An encrypted one
|
|
37
|
-
* its mailbox's keys unlocked; when it can't be, it prints a line saying so rather than the ciphertext. `client` is the explicit `ApiClient` every
|
|
36
|
+
* One earlier message of a conversation as it prints: the body the reading pane would show. An encrypted one - end to end, or sealed to its mailbox's
|
|
37
|
+
* zero-access key, whose subject and body the server doesn't hold readable - is decrypted here when this device has its mailbox's keys unlocked; when it can't be, it prints a line saying so rather than the ciphertext. `client` is the explicit `ApiClient` every
|
|
38
38
|
* request goes through when given (see `useApiClient()`); without one they are the cookie requests they always were.
|
|
39
39
|
*/
|
|
40
40
|
export async function printableOlderMessage(message, client) {
|
|
41
41
|
const attachments = await listAttachments(message.folderUid, message.uid, client).catch(() => []);
|
|
42
|
-
if (message.encrypted) {
|
|
42
|
+
if (message.encrypted || message.zeroAccess) {
|
|
43
43
|
const unlocked = getUnlockedKeys(message.mailboxUid);
|
|
44
44
|
if (unlocked) {
|
|
45
45
|
const { evaluateMessageSecurity } = await import("../../../../../lib/crypto/messageSecurity.js");
|
|
@@ -11,6 +11,7 @@ import { resolveFolderOfType } from "../../../mail/folderOfType.js";
|
|
|
11
11
|
import { setReadStateMany } from "../../../mail/messageReadState.js";
|
|
12
12
|
import { usePermanentDelete } from "../../../mail/usePermanentDelete.js";
|
|
13
13
|
import { reportFolderName, reportNotice, reportNoun, reportTitle, senderListsHref } from "../../../mail/reportNotices.js";
|
|
14
|
+
import { zeroAccessSourceBase64 } from "../../../mail/zeroAccessSource.js";
|
|
14
15
|
import { blockSender, neverBlockSender, normalizeAddresses, removeSenderRule } from "../../../mail/senderBlocking.js";
|
|
15
16
|
import { notifyApiError } from "../../../notifications/apiErrors.js";
|
|
16
17
|
import { notify } from "../../../notifications/store.js";
|
|
@@ -122,7 +123,9 @@ export function useMessageActions(params) {
|
|
|
122
123
|
await exclusive(async () => {
|
|
123
124
|
let result;
|
|
124
125
|
try {
|
|
125
|
-
|
|
126
|
+
// The server holds only the sealed copy of a zero-access message: its spam filter learns from the source decrypted here.
|
|
127
|
+
const rawMessage = await zeroAccessSourceBase64(message, client);
|
|
128
|
+
result = await reportMessage(message.uid, kind, { alwaysTrustSender, ...(rawMessage ? { rawMessage } : {}) }, client);
|
|
126
129
|
}
|
|
127
130
|
catch (err) {
|
|
128
131
|
if (isNotFound(err)) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Mailbox } from "../../../../lib/mail/mailApi.js";
|
|
3
|
+
export interface KeyExportSettingProps {
|
|
4
|
+
mailbox: Mailbox;
|
|
5
|
+
/** The caller owns the mailbox and isn't impersonating: restapi records an export only for the owner. */
|
|
6
|
+
canManage: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* "Export keys for Outlook/phone" (`lib/crypto/keyExport.ts`): a passphrase-protected `.p12` with the mailbox's zero-access
|
|
10
|
+
* and end-to-end encryption keys - and its signing key, if the user ticks the box - so a mail app that isn't RapidMX can read
|
|
11
|
+
* mail stored encrypted. Asks for the master password again however long the mailbox has been unlocked.
|
|
12
|
+
*/
|
|
13
|
+
export default function KeyExportSetting({ mailbox, canManage }: KeyExportSettingProps): React.JSX.Element;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { exportKeysAsPkcs12, MIN_EXPORT_PASSPHRASE_LENGTH, NothingToExportError } from "../../../../lib/crypto/keyExport.js";
|
|
8
|
+
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
9
|
+
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
10
|
+
import Alert from "../../../../lib/components/feedback/Alert.js";
|
|
11
|
+
import Button from "../../../../lib/components/buttons/Button.js";
|
|
12
|
+
import { notifyApiError } from "../../notifications/apiErrors.js";
|
|
13
|
+
import PolicyLockedNotice from "./PolicyLockedNotice.js";
|
|
14
|
+
const INPUT_CLASS = "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface";
|
|
15
|
+
/** How long the file's object URL is kept, since some browsers (WebKit, older Firefox) still read it after the click returns. */
|
|
16
|
+
const DOWNLOAD_URL_LIFETIME_MS = 60000;
|
|
17
|
+
/** Hands `bytes` to the browser as a file download. */
|
|
18
|
+
function saveFile(bytes, filename) {
|
|
19
|
+
const url = URL.createObjectURL(new Blob([bytes], { type: "application/x-pkcs12" }));
|
|
20
|
+
const link = document.createElement("a");
|
|
21
|
+
link.href = url;
|
|
22
|
+
link.download = filename;
|
|
23
|
+
document.body.appendChild(link);
|
|
24
|
+
link.click();
|
|
25
|
+
link.remove();
|
|
26
|
+
setTimeout(() => URL.revokeObjectURL(url), DOWNLOAD_URL_LIFETIME_MS);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* "Export keys for Outlook/phone" (`lib/crypto/keyExport.ts`): a passphrase-protected `.p12` with the mailbox's zero-access
|
|
30
|
+
* and end-to-end encryption keys - and its signing key, if the user ticks the box - so a mail app that isn't RapidMX can read
|
|
31
|
+
* mail stored encrypted. Asks for the master password again however long the mailbox has been unlocked.
|
|
32
|
+
*/
|
|
33
|
+
export default function KeyExportSetting({ mailbox, canManage }) {
|
|
34
|
+
const client = useApiClient();
|
|
35
|
+
const [open, setOpen] = useState(false);
|
|
36
|
+
const [password, setPassword] = useState("");
|
|
37
|
+
const [passphrase, setPassphrase] = useState("");
|
|
38
|
+
const [confirmPassphrase, setConfirmPassphrase] = useState("");
|
|
39
|
+
const [includeSigningKey, setIncludeSigningKey] = useState(false);
|
|
40
|
+
const [exporting, setExporting] = useState(false);
|
|
41
|
+
const [error, setError] = useState(null);
|
|
42
|
+
function reset() {
|
|
43
|
+
setOpen(false);
|
|
44
|
+
setPassword("");
|
|
45
|
+
setPassphrase("");
|
|
46
|
+
setConfirmPassphrase("");
|
|
47
|
+
setIncludeSigningKey(false);
|
|
48
|
+
setError(null);
|
|
49
|
+
}
|
|
50
|
+
async function handleExport(e) {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
if (passphrase.length < MIN_EXPORT_PASSPHRASE_LENGTH) {
|
|
53
|
+
setError(`The file's passphrase must be at least ${MIN_EXPORT_PASSPHRASE_LENGTH} characters.`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (passphrase !== confirmPassphrase) {
|
|
57
|
+
setError("The passphrases don't match.");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
setExporting(true);
|
|
61
|
+
setError(null);
|
|
62
|
+
try {
|
|
63
|
+
const result = await exportKeysAsPkcs12({
|
|
64
|
+
mailboxUid: mailbox.uid,
|
|
65
|
+
mailboxAddress: mailbox.primarySmtpAddress,
|
|
66
|
+
mailboxKeys: mailbox.keys ?? [],
|
|
67
|
+
masterPassword: password,
|
|
68
|
+
exportPassphrase: passphrase,
|
|
69
|
+
includeSigningKey,
|
|
70
|
+
}, client);
|
|
71
|
+
saveFile(result.pkcs12, `${mailbox.primarySmtpAddress}-keys.p12`);
|
|
72
|
+
reset();
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (err instanceof ApiRequestError) {
|
|
76
|
+
notifyApiError(err, "Couldn't export your keys");
|
|
77
|
+
}
|
|
78
|
+
else if (err instanceof NothingToExportError || (err instanceof Error && err.message.startsWith("This mailbox has no password"))) {
|
|
79
|
+
setError(err.message);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
setError("Incorrect password.");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
setExporting(false);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Export keys for Outlook or a phone" }), _jsx("p", { className: "text-sm text-text-muted mb-2", children: "Mail apps other than RapidMX need your keys to read encrypted mail. The exported file holds your zero-access and end-to-end encryption keys, protected by a passphrase you choose. Anyone with the file and its passphrase can read all of this mailbox\u2019s mail, so keep it safe and delete it once it\u2019s installed." }), !canManage ? (_jsx(PolicyLockedNotice, { children: "Only this mailbox\u2019s owner, signed in as themselves, can export its keys." })) : !open ? (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setOpen(true), children: "Export keys\u2026" })) : (_jsxs("form", { onSubmit: (e) => void handleExport(e), className: "flex flex-col gap-2", children: [_jsx("input", { type: "password", "aria-label": "Encryption password", placeholder: "Encryption password", className: INPUT_CLASS, value: password, onChange: (e) => setPassword(e.target.value), disabled: exporting, autoComplete: "current-password" }), _jsx("input", { type: "password", "aria-label": "File passphrase", placeholder: `File passphrase (at least ${MIN_EXPORT_PASSPHRASE_LENGTH} characters)`, className: INPUT_CLASS, value: passphrase, onChange: (e) => setPassphrase(e.target.value), disabled: exporting, autoComplete: "new-password" }), _jsx("input", { type: "password", "aria-label": "Confirm file passphrase", placeholder: "Confirm file passphrase", className: INPUT_CLASS, value: confirmPassphrase, onChange: (e) => setConfirmPassphrase(e.target.value), disabled: exporting, autoComplete: "new-password" }), _jsxs("label", { className: "flex items-start gap-2 text-sm", children: [_jsx("input", { type: "checkbox", className: "mt-1", checked: includeSigningKey, onChange: (e) => setIncludeSigningKey(e.target.checked), disabled: exporting }), _jsxs("span", { children: [_jsx("span", { className: "font-semibold block", children: "Include my signing key" }), _jsx("span", { className: "text-xs text-text-muted", children: "Only if that app will sign mail you send. Anyone with the file could sign as you." })] })] }), error && _jsx(Alert, { children: error }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "submit", className: "!w-auto", disabled: exporting || !password || !passphrase, children: exporting ? "Exporting…" : "Export" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: reset, disabled: exporting, children: "Cancel" })] })] }))] }));
|
|
90
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type Mailbox } from "../../../../lib/mail/mailApi.js";
|
|
3
|
+
export interface OpenPgpKeySettingProps {
|
|
4
|
+
mailbox: Mailbox;
|
|
5
|
+
/** The caller owns the mailbox and isn't impersonating: only the owner may export its secret key. */
|
|
6
|
+
canManage: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The mailbox's OpenPGP key (restapi `specs/end-to-end_encryption.md` *OpenPGP Interoperability*): what it is for, its
|
|
10
|
+
* fingerprint, a download of its public key (to give a correspondent by hand), and an export of its secret key, protected by a
|
|
11
|
+
* passphrase, for reading this mailbox's OpenPGP mail in another app. Shows nothing while the mailbox has no OpenPGP key.
|
|
12
|
+
*/
|
|
13
|
+
export default function OpenPgpKeySetting({ mailbox, canManage }: OpenPgpKeySettingProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { findActivePublicKey } from "../../../../lib/crypto/keyvaultApi.js";
|
|
8
|
+
import { MIN_EXPORT_PASSPHRASE_LENGTH } from "../../../../lib/crypto/keyExport.js";
|
|
9
|
+
import { exportOpenPgpSecretKey } from "../../../../lib/crypto/openPgpKey.js";
|
|
10
|
+
import { getMailbox, updateMailbox } from "../../../../lib/mail/mailApi.js";
|
|
11
|
+
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
12
|
+
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
13
|
+
import Alert from "../../../../lib/components/feedback/Alert.js";
|
|
14
|
+
import Button from "../../../../lib/components/buttons/Button.js";
|
|
15
|
+
import { notifyApiError } from "../../notifications/apiErrors.js";
|
|
16
|
+
import PolicyLockedNotice from "./PolicyLockedNotice.js";
|
|
17
|
+
import { useSettingsShell } from "./layout/SettingsShell.js";
|
|
18
|
+
const INPUT_CLASS = "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface";
|
|
19
|
+
/** How long a download's object URL is kept, since some browsers (WebKit, older Firefox) still read it after the click returns. */
|
|
20
|
+
const DOWNLOAD_URL_LIFETIME_MS = 60000;
|
|
21
|
+
/** Hands `text` to the browser as a file download. */
|
|
22
|
+
function saveText(text, filename) {
|
|
23
|
+
const url = URL.createObjectURL(new Blob([text], { type: "application/pgp-keys" }));
|
|
24
|
+
const link = document.createElement("a");
|
|
25
|
+
link.href = url;
|
|
26
|
+
link.download = filename;
|
|
27
|
+
document.body.appendChild(link);
|
|
28
|
+
link.click();
|
|
29
|
+
link.remove();
|
|
30
|
+
setTimeout(() => URL.revokeObjectURL(url), DOWNLOAD_URL_LIFETIME_MS);
|
|
31
|
+
}
|
|
32
|
+
/** `preference` advertising `format` - S/MIME is the default, so it is advertised by saying nothing. */
|
|
33
|
+
function withPreferredFormat(preference, format) {
|
|
34
|
+
const { preferFormat: _previous, ...rest } = preference ?? { preferEncrypt: "nopreference" };
|
|
35
|
+
return format === "openpgp" ? { ...rest, preferFormat: "openpgp" } : rest;
|
|
36
|
+
}
|
|
37
|
+
/** A fingerprint in groups of four, for reading aloud. */
|
|
38
|
+
function groupFingerprint(fingerprint) {
|
|
39
|
+
return fingerprint.toUpperCase().replace(/(.{4})/g, "$1 ").trim();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The mailbox's OpenPGP key (restapi `specs/end-to-end_encryption.md` *OpenPGP Interoperability*): what it is for, its
|
|
43
|
+
* fingerprint, a download of its public key (to give a correspondent by hand), and an export of its secret key, protected by a
|
|
44
|
+
* passphrase, for reading this mailbox's OpenPGP mail in another app. Shows nothing while the mailbox has no OpenPGP key.
|
|
45
|
+
*/
|
|
46
|
+
export default function OpenPgpKeySetting({ mailbox, canManage }) {
|
|
47
|
+
const client = useApiClient();
|
|
48
|
+
const { patchMailbox } = useSettingsShell();
|
|
49
|
+
const [open, setOpen] = useState(false);
|
|
50
|
+
const [password, setPassword] = useState("");
|
|
51
|
+
const [passphrase, setPassphrase] = useState("");
|
|
52
|
+
const [confirmPassphrase, setConfirmPassphrase] = useState("");
|
|
53
|
+
const [exporting, setExporting] = useState(false);
|
|
54
|
+
const [error, setError] = useState(null);
|
|
55
|
+
const [format, setFormat] = useState(mailbox.encryptPreference?.preferFormat === "openpgp" ? "openpgp" : "smime");
|
|
56
|
+
const [savingFormat, setSavingFormat] = useState(false);
|
|
57
|
+
const key = findActivePublicKey(mailbox.keys ?? [], "encrypt", "openpgp");
|
|
58
|
+
if (!key) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const address = mailbox.primarySmtpAddress;
|
|
62
|
+
function reset() {
|
|
63
|
+
setOpen(false);
|
|
64
|
+
setPassword("");
|
|
65
|
+
setPassphrase("");
|
|
66
|
+
setConfirmPassphrase("");
|
|
67
|
+
setError(null);
|
|
68
|
+
}
|
|
69
|
+
/** Advertises `next` as the format this mailbox would rather receive (restapi *Choosing the format*), to RapidMX servers through
|
|
70
|
+
* key discovery and the `RapidMX-Key` header. */
|
|
71
|
+
async function saveFormat(next) {
|
|
72
|
+
const previous = format;
|
|
73
|
+
setFormat(next);
|
|
74
|
+
setSavingFormat(true);
|
|
75
|
+
try {
|
|
76
|
+
const write = async (current) => {
|
|
77
|
+
const encryptPreference = withPreferredFormat(current.encryptPreference, next);
|
|
78
|
+
const saved = await updateMailbox({ uid: mailbox.uid, version: current.version, encryptPreference }, client);
|
|
79
|
+
return { version: saved.version, encryptPreference: saved.encryptPreference ?? encryptPreference };
|
|
80
|
+
};
|
|
81
|
+
let saved;
|
|
82
|
+
try {
|
|
83
|
+
saved = await write(mailbox);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
// Enrolling a key moves the mailbox's version on: read it again and retry once.
|
|
87
|
+
if (!(err instanceof ApiRequestError && err.status === 409)) {
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
saved = await write(await getMailbox(mailbox.uid, {}, client));
|
|
91
|
+
}
|
|
92
|
+
// Kept in the settings shell's copy of the mailbox, which this page is mounted from again on a switch back to it.
|
|
93
|
+
patchMailbox?.(mailbox.uid, saved);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
setFormat(previous);
|
|
97
|
+
notifyApiError(err, "Couldn't save your preferred encryption format");
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
setSavingFormat(false);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function downloadPublicKey() {
|
|
104
|
+
const { armorPublicKey, readPublicKey } = await import("../../../../lib/crypto/openpgp.js");
|
|
105
|
+
const certificate = await readPublicKey(key.publicKey);
|
|
106
|
+
if (certificate) {
|
|
107
|
+
saveText(armorPublicKey(certificate), `${address}-openpgp-public.asc`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function handleExport(e) {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
if (passphrase.length < MIN_EXPORT_PASSPHRASE_LENGTH) {
|
|
113
|
+
setError(`The file's passphrase must be at least ${MIN_EXPORT_PASSPHRASE_LENGTH} characters.`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (passphrase !== confirmPassphrase) {
|
|
117
|
+
setError("The passphrases don't match.");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
setExporting(true);
|
|
121
|
+
setError(null);
|
|
122
|
+
try {
|
|
123
|
+
const { armored } = await exportOpenPgpSecretKey(mailbox.uid, password, passphrase, client);
|
|
124
|
+
saveText(armored, `${address}-openpgp-secret.asc`);
|
|
125
|
+
reset();
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
if (err instanceof ApiRequestError) {
|
|
129
|
+
notifyApiError(err, "Couldn't export your OpenPGP key");
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
setError("Incorrect password.");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
setExporting(false);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "OpenPGP key" }), _jsx("p", { className: "text-sm text-text-muted mb-2", children: "People who use Proton Mail, Thunderbird or another OpenPGP app can send you encrypted mail with this key, and you can send it to them. RapidMX publishes it for you; you can also give it to someone yourself. RapidMX uses S/MIME whenever a recipient can receive it." }), _jsxs("p", { className: "text-sm mb-2", children: ["Fingerprint: ", _jsx("code", { className: "text-xs break-all", children: groupFingerprint(key.fingerprint) })] }), _jsxs("fieldset", { className: "mb-3", disabled: !canManage || savingFormat, children: [_jsx("legend", { className: "text-sm font-semibold mb-1", children: "Preferred format" }), _jsx("p", { className: "text-xs text-text-muted mb-1", children: "What other RapidMX servers use to encrypt to you when they could use either. Choose OpenPGP if you mostly read this mailbox in an OpenPGP app." }), [
|
|
140
|
+
["smime", "S/MIME (recommended)"],
|
|
141
|
+
["openpgp", "OpenPGP"],
|
|
142
|
+
].map(([value, label]) => (_jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "radio", name: `preferred-format-${mailbox.uid}`, checked: format === value, onChange: () => void saveFormat(value) }), label] }, value)))] }), _jsxs("div", { className: "flex flex-wrap gap-2 mb-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => void downloadPublicKey(), children: "Download public key" }), canManage && !open && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setOpen(true), children: "Export secret key\u2026" }))] }), !canManage ? (_jsx(PolicyLockedNotice, { children: "Only this mailbox\u2019s owner, signed in as themselves, can export its secret key." })) : (open && (_jsxs("form", { onSubmit: (e) => void handleExport(e), className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-sm text-text-muted", children: "Anyone with the exported file and its passphrase can read this mailbox\u2019s OpenPGP mail and sign as you." }), _jsx("input", { type: "password", "aria-label": "Encryption password", placeholder: "Encryption password", className: INPUT_CLASS, value: password, onChange: (e) => setPassword(e.target.value), disabled: exporting, autoComplete: "current-password" }), _jsx("input", { type: "password", "aria-label": "Key passphrase", placeholder: `Key passphrase (at least ${MIN_EXPORT_PASSPHRASE_LENGTH} characters)`, className: INPUT_CLASS, value: passphrase, onChange: (e) => setPassphrase(e.target.value), disabled: exporting, autoComplete: "new-password" }), _jsx("input", { type: "password", "aria-label": "Confirm key passphrase", placeholder: "Confirm key passphrase", className: INPUT_CLASS, value: confirmPassphrase, onChange: (e) => setConfirmPassphrase(e.target.value), disabled: exporting, autoComplete: "new-password" }), error && _jsx(Alert, { children: error }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "submit", className: "!w-auto", loading: exporting, disabled: exporting || !password, children: "Export" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: reset, disabled: exporting, children: "Cancel" })] })] })))] }));
|
|
143
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface PolicyLockedNoticeProps {
|
|
3
|
+
/** Why the setting next to this notice can't be changed - e.g. that an administrator turned it off for the server. */
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Explains why a settings control is disabled by a policy the user can't change themselves (an administrator's server-wide
|
|
8
|
+
* setting, say), shown right under that control. A disabled control with no reason reads as a defect; this says who decided
|
|
9
|
+
* and that the user isn't the one who can undo it.
|
|
10
|
+
*/
|
|
11
|
+
export default function PolicyLockedNotice({ children }: PolicyLockedNoticeProps): React.JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Explains why a settings control is disabled by a policy the user can't change themselves (an administrator's server-wide
|
|
4
|
+
* setting, say), shown right under that control. A disabled control with no reason reads as a defect; this says who decided
|
|
5
|
+
* and that the user isn't the one who can undo it.
|
|
6
|
+
*/
|
|
7
|
+
export default function PolicyLockedNotice({ children }) {
|
|
8
|
+
return (_jsx("p", { role: "note", className: "mt-1.5 text-xs text-text-muted border-l-2 border-border pl-2", children: children }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type Mailbox } from "../../../../lib/mail/mailApi.js";
|
|
3
|
+
/** The data types this client can protect with zero-access encryption - each joins as its protection is implemented end to end. */
|
|
4
|
+
export declare const ZERO_ACCESS_SUPPORTED_TYPES: readonly ["mail", "calendar", "contacts", "tasks"];
|
|
5
|
+
export interface ZeroAccessSettingProps {
|
|
6
|
+
mailbox: Mailbox;
|
|
7
|
+
/** The caller owns the mailbox and isn't impersonating: only then can the setting be changed (restapi drops anybody else's value). */
|
|
8
|
+
canManage: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* "Enable zero-access encryption" for one mailbox (restapi `specs/search.md` §3): unencrypted mail is encrypted to the
|
|
12
|
+
* mailbox's zero-access key as it arrives and the server keeps no readable copy. On by default. Shown disabled, with the
|
|
13
|
+
* reason, when the administrator has turned "Allow zero-access encryption" off for the server, or when the server doesn't
|
|
14
|
+
* support it. A shared mailbox is covered too: its members read it through member key wraps (`sharedMailboxKeys.ts`).
|
|
15
|
+
*/
|
|
16
|
+
export default function ZeroAccessSetting({ mailbox, canManage }: ZeroAccessSettingProps): React.JSX.Element;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { getEncryptionPolicy } from "../../../../lib/crypto/keyvaultApi.js";
|
|
8
|
+
import { getMailbox, updateMailbox } from "../../../../lib/mail/mailApi.js";
|
|
9
|
+
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
10
|
+
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
11
|
+
import { notifyApiError } from "../../notifications/apiErrors.js";
|
|
12
|
+
import { useSettingsShell } from "./layout/SettingsShell.js";
|
|
13
|
+
import PolicyLockedNotice from "./PolicyLockedNotice.js";
|
|
14
|
+
import ZeroAccessTypes from "./ZeroAccessTypes.js";
|
|
15
|
+
/** The data types this client can protect with zero-access encryption - each joins as its protection is implemented end to end. */
|
|
16
|
+
export const ZERO_ACCESS_SUPPORTED_TYPES = ["mail", "calendar", "contacts", "tasks"];
|
|
17
|
+
/**
|
|
18
|
+
* "Enable zero-access encryption" for one mailbox (restapi `specs/search.md` §3): unencrypted mail is encrypted to the
|
|
19
|
+
* mailbox's zero-access key as it arrives and the server keeps no readable copy. On by default. Shown disabled, with the
|
|
20
|
+
* reason, when the administrator has turned "Allow zero-access encryption" off for the server, or when the server doesn't
|
|
21
|
+
* support it. A shared mailbox is covered too: its members read it through member key wraps (`sharedMailboxKeys.ts`).
|
|
22
|
+
*/
|
|
23
|
+
export default function ZeroAccessSetting({ mailbox, canManage }) {
|
|
24
|
+
const client = useApiClient();
|
|
25
|
+
const { patchMailbox } = useSettingsShell();
|
|
26
|
+
const [enabled, setEnabled] = useState(mailbox.zeroAccessEnabled !== false);
|
|
27
|
+
const [blocker, setBlocker] = useState(undefined);
|
|
28
|
+
const [saving, setSaving] = useState(false);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
let cancelled = false;
|
|
31
|
+
void (async () => {
|
|
32
|
+
const policy = await getEncryptionPolicy(client).catch(() => undefined);
|
|
33
|
+
let next = null;
|
|
34
|
+
if (policy?.allowZeroAccess === undefined) {
|
|
35
|
+
next = "unsupported";
|
|
36
|
+
}
|
|
37
|
+
else if (!policy.allowZeroAccess) {
|
|
38
|
+
next = "disabled_by_admin";
|
|
39
|
+
}
|
|
40
|
+
if (!cancelled) {
|
|
41
|
+
setBlocker(next);
|
|
42
|
+
}
|
|
43
|
+
})();
|
|
44
|
+
return () => {
|
|
45
|
+
cancelled = true;
|
|
46
|
+
};
|
|
47
|
+
}, [mailbox.uid, client]);
|
|
48
|
+
async function save(next) {
|
|
49
|
+
setSaving(true);
|
|
50
|
+
const previous = enabled;
|
|
51
|
+
setEnabled(next);
|
|
52
|
+
try {
|
|
53
|
+
const write = (version) => updateMailbox({ uid: mailbox.uid, version, zeroAccessEnabled: next }, client);
|
|
54
|
+
let saved;
|
|
55
|
+
try {
|
|
56
|
+
saved = await write(mailbox.version);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
// Enrolling a key (which happens on unlock) moves the mailbox's version on, so the copy this page was given may be
|
|
60
|
+
// behind: read it again and retry once.
|
|
61
|
+
if (!(err instanceof ApiRequestError && err.status === 409)) {
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
saved = await write((await getMailbox(mailbox.uid, {}, client)).version);
|
|
65
|
+
}
|
|
66
|
+
// The server's answer: turning it on decides every type the owner left undecided (restapi `settleZeroAccessTypes()`), which
|
|
67
|
+
// "What to protect" then shows.
|
|
68
|
+
patchMailbox?.(mailbox.uid, { zeroAccessEnabled: next, zeroAccessTypes: saved.zeroAccessTypes ?? mailbox.zeroAccessTypes, version: saved.version });
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
setEnabled(previous);
|
|
72
|
+
notifyApiError(err, "Couldn't save the zero-access encryption setting");
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
setSaving(false);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const locked = blocker !== null;
|
|
79
|
+
return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Zero-access encryption" }), _jsxs("label", { className: "flex items-start gap-2 text-sm", children: [_jsx("input", { type: "checkbox", className: "mt-1", checked: !locked && enabled, disabled: !canManage || locked || saving, onChange: (e) => void save(e.target.checked) }), _jsxs("span", { children: [_jsx("span", { className: "font-semibold block", children: "Enable zero-access encryption" }), _jsx("span", { className: "text-xs text-text-muted", children: "Mail that arrives unencrypted is encrypted the moment it is received, and the server keeps no copy it can read - nobody else, administrators included, can read it. You unlock it with your encryption password, and search of message contents happens on your device. Outlook and phone mail apps need your exported keys to read it. Turning this off only affects mail that arrives afterwards." })] })] }), !locked && enabled && _jsx(ZeroAccessTypes, { mailbox: mailbox, types: ZERO_ACCESS_SUPPORTED_TYPES, canManage: canManage, disabled: saving }), blocker === "disabled_by_admin" && (_jsx(PolicyLockedNotice, { children: "Your administrator has turned off zero-access encryption for this server, so new mail is stored without it. Mail already encrypted stays encrypted." })), blocker === "unsupported" && _jsx(PolicyLockedNotice, { children: "This server doesn\u2019t offer zero-access encryption." }), !mailbox.ownerUserUid && !locked && (_jsx(PolicyLockedNotice, { children: "This shared mailbox has no owner: it is protected for its members, whose apps share its key with each other." })), !canManage && !locked && mailbox.ownerUserUid && (_jsx(PolicyLockedNotice, { children: "Only this mailbox\u2019s owner, signed in as themselves, can change this." }))] }));
|
|
80
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type Mailbox, type ZeroAccessDataType, type ZeroAccessTypes as ChosenTypes } from "../../../../lib/mail/mailApi.js";
|
|
3
|
+
/** What each data type's protection covers, what stays readable and why, and what it breaks on a phone or Outlook - the copy restapi
|
|
4
|
+
* `specs/search.md` §3 *What It Covers* requires settings to state plainly. */
|
|
5
|
+
export declare const ZERO_ACCESS_TYPE_INFO: Record<ZeroAccessDataType, {
|
|
6
|
+
label: string;
|
|
7
|
+
encrypted: string;
|
|
8
|
+
readable: string;
|
|
9
|
+
onDevices: string;
|
|
10
|
+
}>;
|
|
11
|
+
/** Whether `type` is included: the owner's choice, else (undecided) mail on and the rest off - restapi's `zeroAccessTypeIncluded()`. */
|
|
12
|
+
export declare function typeIncluded(types: ChosenTypes | undefined, type: ZeroAccessDataType): boolean;
|
|
13
|
+
/** The names of the owner's ActiveSync devices that sync `type`, and Outlook when it syncs the mailbox. */
|
|
14
|
+
export declare function devicesSyncing(mailbox: Pick<Mailbox, "syncClients">, type: ZeroAccessDataType): string[];
|
|
15
|
+
export interface ZeroAccessTypesProps {
|
|
16
|
+
mailbox: Mailbox;
|
|
17
|
+
/** The data types this client can protect, in order - each one's switch appears once its protection is implemented. */
|
|
18
|
+
types: readonly ZeroAccessDataType[];
|
|
19
|
+
/** The owner, not impersonating: only they change it. */
|
|
20
|
+
canManage: boolean;
|
|
21
|
+
/** Zero-access is being turned on: the server is still deciding the undecided types, so nothing is changed before it has. */
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* "What to protect": one switch per data type zero-access encryption covers, each saying what it encrypts and - for a type one of the
|
|
26
|
+
* owner's ActiveSync devices or Outlook syncs - plainly what that device will no longer show (restapi `specs/search.md` §3 *What It
|
|
27
|
+
* Covers*). Then the table of what stays readable to the server and why.
|
|
28
|
+
*/
|
|
29
|
+
export default function ZeroAccessTypes({ mailbox, types, canManage, disabled }: ZeroAccessTypesProps): React.JSX.Element;
|