@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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/**
|
|
6
|
+
* Recipient suggestions that include contacts zero-access encryption protects. Their names are sealed (restapi `specs/search.md` §3 *What
|
|
7
|
+
* It Covers*), so the server's contact search finds them only by address; this matches their opened names on the device and puts them
|
|
8
|
+
* first. Names are opened only - never the rest of a contact - and kept for ten minutes, and only while the mailbox is unlocked: its lock
|
|
9
|
+
* (and signing out) forgets them, as does a search that finds it locked. Reading them never holds up the server's suggestions: what isn't read by the time those arrive (plus
|
|
10
|
+
* `PROTECTED_NAMES_WAIT_MS`) is left to the next keystroke.
|
|
11
|
+
*/
|
|
12
|
+
import { type Contact, listContacts, PINNED_FINGERPRINT_PAGE_SIZE } from "../../../../../lib/contacts/contactsApi.js";
|
|
13
|
+
import { getUnlockedKeys, subscribeKeySession } from "../../../../../lib/crypto/keySession.js";
|
|
14
|
+
import {
|
|
15
|
+
type ContactSuggestionOptions,
|
|
16
|
+
fetchRecipientSuggestions,
|
|
17
|
+
mergeRecipientSuggestions,
|
|
18
|
+
RECIPIENT_SUGGESTION_DEFAULT_LIMIT,
|
|
19
|
+
type RecipientSuggestion,
|
|
20
|
+
} from "../../../../../lib/mail/directoryApi.js";
|
|
21
|
+
import { listFolders } from "../../../../../lib/mail/mailApi.js";
|
|
22
|
+
import type { ApiClient } from "../../../../../lib/util/api.js";
|
|
23
|
+
import { isProtectedRecord, openSealedRecords } from "../../../../../lib/zeroAccess/sealedRecords.js";
|
|
24
|
+
import { onSigningOut } from "./composeFlushRegistry.js";
|
|
25
|
+
|
|
26
|
+
/** How long a mailbox's protected contact names are kept: reading them is a crawl of the whole address book. */
|
|
27
|
+
export const PROTECTED_NAMES_TTL_MS = 10 * 60_000;
|
|
28
|
+
|
|
29
|
+
/** How long suggestions wait, once the server's have arrived, for the protected contacts still being read. */
|
|
30
|
+
export const PROTECTED_NAMES_WAIT_MS = 250;
|
|
31
|
+
|
|
32
|
+
/** At most this many contacts are read per contacts folder. */
|
|
33
|
+
const MAX_PAGES = 20;
|
|
34
|
+
|
|
35
|
+
const cache = new Map<string, { loadedAt: number; contacts: Promise<Contact[]> }>();
|
|
36
|
+
|
|
37
|
+
/** Forgets every mailbox's names (signing out, or the test seam). */
|
|
38
|
+
export function clearProtectedContactNames(): void {
|
|
39
|
+
cache.clear();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onSigningOut(clearProtectedContactNames);
|
|
43
|
+
|
|
44
|
+
let watchingKeys = false;
|
|
45
|
+
|
|
46
|
+
/** A mailbox's names go the moment it locks, so nothing opened outlives its key session. Subscribed with the first crawl, not on import. */
|
|
47
|
+
function forgetOnLock(): void {
|
|
48
|
+
if (watchingKeys) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
watchingKeys = true;
|
|
52
|
+
subscribeKeySession(({ mailboxUid, state }) => {
|
|
53
|
+
if (state === "locked") {
|
|
54
|
+
cache.delete(mailboxUid);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `mailboxUid`'s protected contacts with their names opened. */
|
|
60
|
+
async function loadProtectedContacts(mailboxUid: string, client: ApiClient | undefined): Promise<Contact[]> {
|
|
61
|
+
const found: Contact[] = [];
|
|
62
|
+
// The folders side by side, each one's pages in turn.
|
|
63
|
+
await Promise.all(
|
|
64
|
+
(await listFolders(mailboxUid, client))
|
|
65
|
+
.filter((f) => f.type === "contacts")
|
|
66
|
+
.map(async (folder) => {
|
|
67
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
68
|
+
const batch = await listContacts(folder.uid, { limit: PINNED_FINGERPRINT_PAGE_SIZE, page }, client);
|
|
69
|
+
found.push(...batch.filter((contact) => isProtectedRecord(contact)));
|
|
70
|
+
if (batch.length < PINNED_FINGERPRINT_PAGE_SIZE) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
return openSealedRecords(found, ["displayName"]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function protectedContacts(mailboxUid: string, client: ApiClient | undefined): Promise<Contact[]> {
|
|
80
|
+
let entry = cache.get(mailboxUid);
|
|
81
|
+
if (!entry || Date.now() - entry.loadedAt > PROTECTED_NAMES_TTL_MS) {
|
|
82
|
+
forgetOnLock();
|
|
83
|
+
const created = { loadedAt: Date.now(), contacts: loadProtectedContacts(mailboxUid, client) };
|
|
84
|
+
created.contacts.catch(() => cache.delete(mailboxUid));
|
|
85
|
+
cache.set(mailboxUid, created);
|
|
86
|
+
entry = created;
|
|
87
|
+
}
|
|
88
|
+
return entry.contacts;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Every address of the protected contacts of `mailboxUid` whose opened name contains `query` (case-insensitively). */
|
|
92
|
+
export async function searchProtectedContactNames(mailboxUid: string, query: string, client?: ApiClient): Promise<RecipientSuggestion[]> {
|
|
93
|
+
const wanted = query.trim().toLowerCase();
|
|
94
|
+
if (wanted.length < 2) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
if (!getUnlockedKeys(mailboxUid)?.zeroAccessKeys?.length) {
|
|
98
|
+
// Locked: nothing opened is kept, and nothing could be opened now.
|
|
99
|
+
cache.delete(mailboxUid);
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
return (await protectedContacts(mailboxUid, client))
|
|
103
|
+
.filter((contact) => contact.displayName.toLowerCase().includes(wanted))
|
|
104
|
+
.flatMap((contact) => contact.emails.map((email) => ({ displayName: contact.displayName, address: email.address, kind: "contact" as const })));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** `fetchRecipientSuggestions()` with the composing mailbox's protected contacts matched by name on the device, first - those read by the
|
|
108
|
+
* time the server's suggestions are in (plus `PROTECTED_NAMES_WAIT_MS`): a first search of a large address book shows the server's alone
|
|
109
|
+
* rather than waiting for the crawl, which goes on for the next keystroke. A failure to read them only leaves them out. */
|
|
110
|
+
export async function fetchSuggestionsWithProtectedContacts(
|
|
111
|
+
query: string,
|
|
112
|
+
options: ContactSuggestionOptions = {},
|
|
113
|
+
client?: ApiClient,
|
|
114
|
+
): Promise<RecipientSuggestion[]> {
|
|
115
|
+
const local: Promise<RecipientSuggestion[]> = options.mailboxUid ? searchProtectedContactNames(options.mailboxUid, query, client).catch(() => []) : Promise.resolve([]);
|
|
116
|
+
const server = await fetchRecipientSuggestions(query, options, client);
|
|
117
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
118
|
+
const ready = await Promise.race([local, new Promise<RecipientSuggestion[]>((resolve) => (timer = setTimeout(() => resolve([]), PROTECTED_NAMES_WAIT_MS)))]);
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
return mergeRecipientSuggestions(ready, server, options.limit ?? RECIPIENT_SUGGESTION_DEFAULT_LIMIT);
|
|
121
|
+
}
|
|
@@ -94,10 +94,10 @@ function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
|
94
94
|
/**
|
|
95
95
|
* Starts fetching what a Reply/Forward of `message` will quote, without waiting for the click - called when the pointer or
|
|
96
96
|
* keyboard reaches a Reply/Reply All/Forward button - so the request is usually already answered when it is clicked.
|
|
97
|
-
* Nothing to fetch for an encrypted message (`loadOriginalMessage()` never asks the server for one's body).
|
|
97
|
+
* Nothing to fetch for an encrypted or zero-access message (`loadOriginalMessage()` never asks the server for one's body).
|
|
98
98
|
*/
|
|
99
99
|
export function prefetchOriginalMessage(message: Message, client?: ApiClient): void {
|
|
100
|
-
if (!message.encrypted) {
|
|
100
|
+
if (!message.encrypted && !message.zeroAccess) {
|
|
101
101
|
void fetchContentHtml(message.uid, client);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
@@ -201,7 +201,8 @@ export async function loadOriginalMessage(
|
|
|
201
201
|
? [...headerRecipients(protectedHeaders.to, "to"), ...headerRecipients(protectedHeaders.cc, "cc")]
|
|
202
202
|
: [];
|
|
203
203
|
const result: OriginalMessage = { body: recovered ?? {}, ...(protectedRecipients.length > 0 ? { recipients: protectedRecipients } : {}) };
|
|
204
|
-
|
|
204
|
+
// The server holds only ciphertext of an encrypted or zero-access message: its body is what `security` decrypted, or nothing.
|
|
205
|
+
if (message.encrypted || message.zeroAccess) {
|
|
205
206
|
return result;
|
|
206
207
|
}
|
|
207
208
|
let needsRaw = !!options.recipients && result.recipients === undefined;
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import { useCallback, useEffect, useReducer } from "react";
|
|
6
6
|
import { MessageInvite, 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 { ApiClient } from "../../../../../lib/util/api.js";
|
|
8
10
|
import { useApiClient } from "../../../../../lib/util/apiClientContext.js";
|
|
9
11
|
|
|
@@ -51,7 +53,9 @@ function normalize(invite: MessageInvite | null): MessageInvite | null {
|
|
|
51
53
|
*/
|
|
52
54
|
export function loadInvite(uid: string, client?: ApiClient): Promise<MessageInvite | null> {
|
|
53
55
|
const existing = entries.get(uid);
|
|
54
|
-
|
|
56
|
+
// "No invitation" for a zero-access message is asked again once its calendar file has been decrypted here (the list asks before that).
|
|
57
|
+
const stale = existing?.value === null && sealedInviteCalendar(uid) !== undefined;
|
|
58
|
+
if (existing && !stale && (existing.value === undefined || Date.now() - existing.at < INVITE_TTL_MS)) {
|
|
55
59
|
return existing.promise;
|
|
56
60
|
}
|
|
57
61
|
const entry: Entry = {
|
|
@@ -120,3 +124,15 @@ export function useMessageInvite(uid: string, enabled = true): { invite: Message
|
|
|
120
124
|
const value = enabled ? entries.get(uid)?.value : undefined;
|
|
121
125
|
return { invite: value ?? null, settled: value !== undefined, setInvite };
|
|
122
126
|
}
|
|
127
|
+
|
|
128
|
+
onSigningOut(clearSealedInvites);
|
|
129
|
+
onSigningOut(clearInviteCache);
|
|
130
|
+
// What the server read from a zero-access message's decrypted calendar file (its summary, description) goes with the file when the mailbox
|
|
131
|
+
// locks: mounted cards, chips and popovers draw nothing until it is read again.
|
|
132
|
+
onSealedInvitesForgotten((uids) => {
|
|
133
|
+
for (const uid of uids) {
|
|
134
|
+
if (entries.delete(uid)) {
|
|
135
|
+
notify(uid);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
@@ -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 { LiveUpdates, 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";
|
|
@@ -538,6 +539,7 @@ export default function MailShell({
|
|
|
538
539
|
// the impersonated user's own mailbox: an administrator must never choose its password/recovery codes.
|
|
539
540
|
canProvision={!impersonating && activeMailbox?.ownerUserUid === userUid}
|
|
540
541
|
>
|
|
542
|
+
<SharedMailboxKeyManager mailboxes={mailboxes} userUid={userUid} />
|
|
541
543
|
<LocalIndexLifecycle
|
|
542
544
|
mailboxUid={activeMailboxUid}
|
|
543
545
|
mailboxKeys={activeMailbox?.keys}
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
PINNED_FINGERPRINT_PAGE_SIZE,
|
|
29
29
|
SignerKeyState,
|
|
30
30
|
listContacts,
|
|
31
|
+
pinnedOpenPgpKeysFor,
|
|
31
32
|
pinnedSigningFingerprintsFor,
|
|
32
33
|
signerKeyStateFor,
|
|
33
34
|
} from "../../../../lib/contacts/contactsApi.js";
|
|
@@ -35,6 +36,7 @@ import { keyPinnedSince } from "../contacts/contactKeys.js";
|
|
|
35
36
|
import { listFolders } from "../../../../lib/mail/mailApi.js";
|
|
36
37
|
import { subscribeKeySession } from "../../../../lib/crypto/keySession.js";
|
|
37
38
|
import type { ApiClient } from "../../../../lib/util/api.js";
|
|
39
|
+
import type { SignerPins } from "../../../../lib/crypto/messageSecurity.js";
|
|
38
40
|
|
|
39
41
|
/** How long one mailbox's loaded contact list is reused before it is fetched again. */
|
|
40
42
|
export const CONTACTS_CACHE_TTL_MS = 60_000;
|
|
@@ -120,12 +122,21 @@ export async function getPinnedSignerFingerprints(mailboxUid: string, address: s
|
|
|
120
122
|
return pinnedSigningFingerprintsFor(await cachedMailboxContacts(mailboxUid, client), address);
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
/** The sender `address`'s pinned signing fingerprints and pinned OpenPGP certificates from `mailboxUid`'s contacts - what
|
|
126
|
+
* `evaluateMessageSecurity()` takes as `SignerPins`. Rejects if they can't be loaded. */
|
|
127
|
+
export async function getPinnedSigners(mailboxUid: string, address: string, client?: ApiClient): Promise<SignerPins> {
|
|
128
|
+
const contacts = await cachedMailboxContacts(mailboxUid, client);
|
|
129
|
+
return { fingerprints: pinnedSigningFingerprintsFor(contacts, address), openPgpKeys: pinnedOpenPgpKeysFor(contacts, address) };
|
|
130
|
+
}
|
|
131
|
+
|
|
123
132
|
/** A sender's signing-key state (`signerKeyStateFor()`) plus what a key-changed notice needs to show and link. */
|
|
124
133
|
export interface SenderKeyState extends SignerKeyState {
|
|
125
134
|
/** The contact holding the first pinned signing key, else the one holding the conflict, else the first match. */
|
|
126
135
|
contactUid?: string;
|
|
127
136
|
/** When the first pinned signing key started being trusted - see `keyPinnedSince()`. */
|
|
128
137
|
pinnedSince?: number;
|
|
138
|
+
/** When the first pinned OpenPGP certificate (`openPgp[0]`) started being trusted. */
|
|
139
|
+
openPgpPinnedSince?: number;
|
|
129
140
|
}
|
|
130
141
|
|
|
131
142
|
/** The sender `address`'s signing-key state from `mailboxUid`'s (cached) contacts, for a "signing key changed" comparison
|
|
@@ -142,9 +153,12 @@ export async function getSignerKeyState(mailboxUid: string, address: string, cli
|
|
|
142
153
|
holder ??
|
|
143
154
|
matching.find((candidate) => (candidate.keyConflicts ?? []).some((conflict) => conflict.useType === "sign")) ??
|
|
144
155
|
matching[0];
|
|
156
|
+
const openPgpKey = state.openPgp?.[0];
|
|
157
|
+
const openPgpHolder = openPgpKey && matching.find((candidate) => (candidate.keys ?? []).includes(openPgpKey));
|
|
145
158
|
return {
|
|
146
159
|
...state,
|
|
147
160
|
...(contact ? { contactUid: contact.uid } : {}),
|
|
148
161
|
...(holder ? { pinnedSince: keyPinnedSince(holder, pinned) } : {}),
|
|
162
|
+
...(openPgpHolder ? { openPgpPinnedSince: keyPinnedSince(openPgpHolder, openPgpKey) } : {}),
|
|
149
163
|
};
|
|
150
164
|
}
|
|
@@ -39,13 +39,13 @@ export async function olderConversationMessages(message: Message, client?: ApiCl
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* One earlier message of a conversation as it prints: the body the reading pane would show. An encrypted one
|
|
43
|
-
* 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
|
|
42
|
+
* 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
|
|
43
|
+
* 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
|
|
44
44
|
* request goes through when given (see `useApiClient()`); without one they are the cookie requests they always were.
|
|
45
45
|
*/
|
|
46
46
|
export async function printableOlderMessage(message: Message, client?: ApiClient): Promise<PrintableMessage> {
|
|
47
47
|
const attachments = await listAttachments(message.folderUid, message.uid, client).catch(() => []);
|
|
48
|
-
if (message.encrypted) {
|
|
48
|
+
if (message.encrypted || message.zeroAccess) {
|
|
49
49
|
const unlocked = getUnlockedKeys(message.mailboxUid);
|
|
50
50
|
if (unlocked) {
|
|
51
51
|
const { evaluateMessageSecurity } = await import("../../../../../lib/crypto/messageSecurity.js");
|
|
@@ -28,6 +28,7 @@ import { resolveFolderOfType } from "../../../mail/folderOfType.js";
|
|
|
28
28
|
import { setReadStateMany } from "../../../mail/messageReadState.js";
|
|
29
29
|
import { usePermanentDelete } from "../../../mail/usePermanentDelete.js";
|
|
30
30
|
import { reportFolderName, reportNotice, reportNoun, reportTitle, senderListsHref } from "../../../mail/reportNotices.js";
|
|
31
|
+
import { zeroAccessSourceBase64 } from "../../../mail/zeroAccessSource.js";
|
|
31
32
|
import { blockSender, neverBlockSender, normalizeAddresses, removeSenderRule } from "../../../mail/senderBlocking.js";
|
|
32
33
|
import { notifyApiError } from "../../../notifications/apiErrors.js";
|
|
33
34
|
import { notify } from "../../../notifications/store.js";
|
|
@@ -190,7 +191,9 @@ export function useMessageActions(params: MessageActionsParams): MessageActions
|
|
|
190
191
|
await exclusive(async () => {
|
|
191
192
|
let result: MessageReportResult;
|
|
192
193
|
try {
|
|
193
|
-
|
|
194
|
+
// The server holds only the sealed copy of a zero-access message: its spam filter learns from the source decrypted here.
|
|
195
|
+
const rawMessage: string | undefined = await zeroAccessSourceBase64(message, client);
|
|
196
|
+
result = await reportMessage(message.uid, kind, { alwaysTrustSender, ...(rawMessage ? { rawMessage } : {}) }, client);
|
|
194
197
|
} catch (err) {
|
|
195
198
|
if (isNotFound(err)) {
|
|
196
199
|
await reportByMoving(kind, alwaysTrustSender);
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { useState } from "react";
|
|
6
|
+
import { exportKeysAsPkcs12, MIN_EXPORT_PASSPHRASE_LENGTH, NothingToExportError } from "../../../../lib/crypto/keyExport.js";
|
|
7
|
+
import type { Mailbox } from "../../../../lib/mail/mailApi.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
|
+
|
|
15
|
+
export interface KeyExportSettingProps {
|
|
16
|
+
mailbox: Mailbox;
|
|
17
|
+
/** The caller owns the mailbox and isn't impersonating: restapi records an export only for the owner. */
|
|
18
|
+
canManage: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const INPUT_CLASS = "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface";
|
|
22
|
+
|
|
23
|
+
/** How long the file's object URL is kept, since some browsers (WebKit, older Firefox) still read it after the click returns. */
|
|
24
|
+
const DOWNLOAD_URL_LIFETIME_MS = 60_000;
|
|
25
|
+
|
|
26
|
+
/** Hands `bytes` to the browser as a file download. */
|
|
27
|
+
function saveFile(bytes: Uint8Array, filename: string): void {
|
|
28
|
+
const url = URL.createObjectURL(new Blob([bytes as BlobPart], { type: "application/x-pkcs12" }));
|
|
29
|
+
const link = document.createElement("a");
|
|
30
|
+
link.href = url;
|
|
31
|
+
link.download = filename;
|
|
32
|
+
document.body.appendChild(link);
|
|
33
|
+
link.click();
|
|
34
|
+
link.remove();
|
|
35
|
+
setTimeout(() => URL.revokeObjectURL(url), DOWNLOAD_URL_LIFETIME_MS);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* "Export keys for Outlook/phone" (`lib/crypto/keyExport.ts`): a passphrase-protected `.p12` with the mailbox's zero-access
|
|
40
|
+
* 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
|
|
41
|
+
* mail stored encrypted. Asks for the master password again however long the mailbox has been unlocked.
|
|
42
|
+
*/
|
|
43
|
+
export default function KeyExportSetting({ mailbox, canManage }: KeyExportSettingProps) {
|
|
44
|
+
const client = useApiClient();
|
|
45
|
+
const [open, setOpen] = useState(false);
|
|
46
|
+
const [password, setPassword] = useState("");
|
|
47
|
+
const [passphrase, setPassphrase] = useState("");
|
|
48
|
+
const [confirmPassphrase, setConfirmPassphrase] = useState("");
|
|
49
|
+
const [includeSigningKey, setIncludeSigningKey] = useState(false);
|
|
50
|
+
const [exporting, setExporting] = useState(false);
|
|
51
|
+
const [error, setError] = useState<string | null>(null);
|
|
52
|
+
|
|
53
|
+
function reset() {
|
|
54
|
+
setOpen(false);
|
|
55
|
+
setPassword("");
|
|
56
|
+
setPassphrase("");
|
|
57
|
+
setConfirmPassphrase("");
|
|
58
|
+
setIncludeSigningKey(false);
|
|
59
|
+
setError(null);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function handleExport(e: React.FormEvent) {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
if (passphrase.length < MIN_EXPORT_PASSPHRASE_LENGTH) {
|
|
65
|
+
setError(`The file's passphrase must be at least ${MIN_EXPORT_PASSPHRASE_LENGTH} characters.`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (passphrase !== confirmPassphrase) {
|
|
69
|
+
setError("The passphrases don't match.");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
setExporting(true);
|
|
73
|
+
setError(null);
|
|
74
|
+
try {
|
|
75
|
+
const result = await exportKeysAsPkcs12(
|
|
76
|
+
{
|
|
77
|
+
mailboxUid: mailbox.uid,
|
|
78
|
+
mailboxAddress: mailbox.primarySmtpAddress,
|
|
79
|
+
mailboxKeys: mailbox.keys ?? [],
|
|
80
|
+
masterPassword: password,
|
|
81
|
+
exportPassphrase: passphrase,
|
|
82
|
+
includeSigningKey,
|
|
83
|
+
},
|
|
84
|
+
client,
|
|
85
|
+
);
|
|
86
|
+
saveFile(result.pkcs12, `${mailbox.primarySmtpAddress}-keys.p12`);
|
|
87
|
+
reset();
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (err instanceof ApiRequestError) {
|
|
90
|
+
notifyApiError(err, "Couldn't export your keys");
|
|
91
|
+
} else if (err instanceof NothingToExportError || (err instanceof Error && err.message.startsWith("This mailbox has no password"))) {
|
|
92
|
+
setError(err.message);
|
|
93
|
+
} else {
|
|
94
|
+
setError("Incorrect password.");
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
setExporting(false);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div>
|
|
103
|
+
<h2 className="text-sm font-semibold mb-2">Export keys for Outlook or a phone</h2>
|
|
104
|
+
<p className="text-sm text-text-muted mb-2">
|
|
105
|
+
Mail apps other than RapidMX need your keys to read encrypted mail. The exported file holds your zero-access and
|
|
106
|
+
end-to-end encryption keys, protected by a passphrase you choose. Anyone with the file and its passphrase can read
|
|
107
|
+
all of this mailbox’s mail, so keep it safe and delete it once it’s installed.
|
|
108
|
+
</p>
|
|
109
|
+
{!canManage ? (
|
|
110
|
+
<PolicyLockedNotice>Only this mailbox’s owner, signed in as themselves, can export its keys.</PolicyLockedNotice>
|
|
111
|
+
) : !open ? (
|
|
112
|
+
<Button type="button" variant="secondary" className="!w-auto" onClick={() => setOpen(true)}>
|
|
113
|
+
Export keys…
|
|
114
|
+
</Button>
|
|
115
|
+
) : (
|
|
116
|
+
<form onSubmit={(e) => void handleExport(e)} className="flex flex-col gap-2">
|
|
117
|
+
<input
|
|
118
|
+
type="password"
|
|
119
|
+
aria-label="Encryption password"
|
|
120
|
+
placeholder="Encryption password"
|
|
121
|
+
className={INPUT_CLASS}
|
|
122
|
+
value={password}
|
|
123
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
124
|
+
disabled={exporting}
|
|
125
|
+
autoComplete="current-password"
|
|
126
|
+
/>
|
|
127
|
+
<input
|
|
128
|
+
type="password"
|
|
129
|
+
aria-label="File passphrase"
|
|
130
|
+
placeholder={`File passphrase (at least ${MIN_EXPORT_PASSPHRASE_LENGTH} characters)`}
|
|
131
|
+
className={INPUT_CLASS}
|
|
132
|
+
value={passphrase}
|
|
133
|
+
onChange={(e) => setPassphrase(e.target.value)}
|
|
134
|
+
disabled={exporting}
|
|
135
|
+
autoComplete="new-password"
|
|
136
|
+
/>
|
|
137
|
+
<input
|
|
138
|
+
type="password"
|
|
139
|
+
aria-label="Confirm file passphrase"
|
|
140
|
+
placeholder="Confirm file passphrase"
|
|
141
|
+
className={INPUT_CLASS}
|
|
142
|
+
value={confirmPassphrase}
|
|
143
|
+
onChange={(e) => setConfirmPassphrase(e.target.value)}
|
|
144
|
+
disabled={exporting}
|
|
145
|
+
autoComplete="new-password"
|
|
146
|
+
/>
|
|
147
|
+
<label className="flex items-start gap-2 text-sm">
|
|
148
|
+
<input
|
|
149
|
+
type="checkbox"
|
|
150
|
+
className="mt-1"
|
|
151
|
+
checked={includeSigningKey}
|
|
152
|
+
onChange={(e) => setIncludeSigningKey(e.target.checked)}
|
|
153
|
+
disabled={exporting}
|
|
154
|
+
/>
|
|
155
|
+
<span>
|
|
156
|
+
<span className="font-semibold block">Include my signing key</span>
|
|
157
|
+
<span className="text-xs text-text-muted">Only if that app will sign mail you send. Anyone with the file could sign as you.</span>
|
|
158
|
+
</span>
|
|
159
|
+
</label>
|
|
160
|
+
{error && <Alert>{error}</Alert>}
|
|
161
|
+
<div className="flex gap-2">
|
|
162
|
+
<Button type="submit" className="!w-auto" disabled={exporting || !password || !passphrase}>
|
|
163
|
+
{exporting ? "Exporting…" : "Export"}
|
|
164
|
+
</Button>
|
|
165
|
+
<Button type="button" variant="secondary" className="!w-auto" onClick={reset} disabled={exporting}>
|
|
166
|
+
Cancel
|
|
167
|
+
</Button>
|
|
168
|
+
</div>
|
|
169
|
+
</form>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|