@rapidmx/web-client 0.36.0 → 0.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/shared/calendar/calendarReminders.ts +35 -0
- package/apps/shared/calendar/sealedEvents.ts +42 -9
- package/apps/shared/calendar/useCalendarReminders.ts +28 -9
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -0
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +29 -1
- package/apps/shared/components/calendar/EventDetails.tsx +13 -2
- package/apps/shared/components/calendar/EventEditor.tsx +124 -17
- package/apps/shared/components/calendar/EventFormParts.tsx +1 -0
- package/apps/shared/components/calendar/EventModal.tsx +19 -2
- package/apps/shared/components/calendar/RequestChangeForm.tsx +4 -1
- package/apps/shared/components/calendar/eventForm.ts +2 -0
- package/apps/shared/components/contacts/ContactCard.tsx +13 -1
- package/apps/shared/components/contacts/ContactDetailPane.tsx +189 -9
- package/apps/shared/components/contacts/ContactForm.tsx +56 -7
- package/apps/shared/components/contacts/KeyChangeReview.tsx +5 -1
- package/apps/shared/components/contacts/contactCardData.ts +6 -3
- package/apps/shared/components/contacts/contactKeys.ts +3 -3
- package/apps/shared/components/layout/AppShell.tsx +34 -5
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +11 -4
- package/apps/shared/components/layout/SharedMailboxKeyManager.tsx +37 -6
- package/apps/shared/components/mail/InviteCard.tsx +11 -1
- package/apps/shared/components/mail/MessageDetailPane.tsx +25 -14
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +9 -2
- package/apps/shared/components/mail/compose/RecipientInput.tsx +2 -1
- package/apps/shared/components/mail/compose/protectedContactSuggestions.ts +121 -0
- package/apps/shared/components/mail/invite/inviteStore.ts +11 -1
- package/apps/shared/components/mail/pinnedSigners.ts +14 -0
- package/apps/shared/components/mail/reading/conversationPrint.ts +3 -3
- package/apps/shared/components/settings/KeyExportSetting.tsx +4 -1
- package/apps/shared/components/settings/OpenPgpKeySetting.tsx +240 -0
- package/apps/shared/components/settings/ZeroAccessSetting.tsx +13 -2
- package/apps/shared/components/settings/ZeroAccessTypes.tsx +175 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +9 -2
- package/apps/shared/mail/outbox/sendJob.ts +349 -50
- package/apps/shared/mail/outbox/sendOutcomes.ts +91 -20
- package/apps/shared/search/LocalIndexLifecycle.tsx +62 -33
- package/apps/shared/search/localIndexBuilder.ts +35 -24
- package/apps/shared/search/localIndexRpcClient.ts +91 -11
- package/apps/shared/search/localIndexSchema.ts +18 -2
- package/apps/shared/search/localIndexTransport.ts +6 -3
- package/apps/shared/search/localIndexWorker.ts +35 -18
- package/apps/shared/search/searchTier2.ts +5 -0
- package/apps/shared/search/zeroAccessIndexSync.ts +232 -40
- package/apps/shared/search/zeroAccessSyncState.ts +70 -0
- package/apps/shared/zeroAccess/ProtectedMark.tsx +19 -0
- package/apps/shared/zeroAccess/useKeysLocked.ts +25 -0
- package/apps/shared/zeroAccess/useOpenedRecord.ts +71 -0
- package/apps/www/_shell.tsx +2 -0
- package/apps/www/calendar/index.tsx +49 -5
- package/apps/www/contacts/index.tsx +67 -11
- package/apps/www/index.tsx +10 -1
- package/apps/www/settings/encryption/index.tsx +16 -1
- package/apps/www/settings/privacy/index.tsx +87 -38
- package/apps/www/tasks/index.tsx +24 -6
- package/dist/apps/shared/calendar/calendarReminders.d.ts +14 -0
- package/dist/apps/shared/calendar/calendarReminders.js +26 -0
- package/dist/apps/shared/calendar/sealedEvents.d.ts +3 -2
- package/dist/apps/shared/calendar/sealedEvents.js +40 -9
- package/dist/apps/shared/calendar/useCalendarReminders.d.ts +2 -1
- package/dist/apps/shared/calendar/useCalendarReminders.js +27 -9
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +3 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +9 -2
- package/dist/apps/shared/components/calendar/EventDetails.js +13 -2
- package/dist/apps/shared/components/calendar/EventEditor.js +115 -18
- package/dist/apps/shared/components/calendar/EventFormParts.js +1 -1
- package/dist/apps/shared/components/calendar/EventModal.d.ts +1 -1
- package/dist/apps/shared/components/calendar/EventModal.js +15 -2
- package/dist/apps/shared/components/calendar/RequestChangeForm.d.ts +3 -0
- package/dist/apps/shared/components/calendar/RequestChangeForm.js +1 -1
- package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -0
- package/dist/apps/shared/components/contacts/ContactCard.js +12 -1
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +2 -1
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +103 -9
- package/dist/apps/shared/components/contacts/ContactForm.d.ts +4 -1
- package/dist/apps/shared/components/contacts/ContactForm.js +37 -7
- package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +3 -1
- package/dist/apps/shared/components/contacts/KeyChangeReview.js +3 -2
- package/dist/apps/shared/components/contacts/contactCardData.js +6 -3
- package/dist/apps/shared/components/contacts/contactKeys.d.ts +1 -1
- package/dist/apps/shared/components/contacts/contactKeys.js +3 -3
- package/dist/apps/shared/components/layout/AppShell.d.ts +18 -1
- package/dist/apps/shared/components/layout/AppShell.js +17 -6
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -4
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.d.ts +3 -1
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.js +30 -4
- package/dist/apps/shared/components/mail/InviteCard.js +11 -1
- package/dist/apps/shared/components/mail/MessageDetailPane.js +15 -11
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +2 -2
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +3 -2
- package/dist/apps/shared/components/mail/compose/protectedContactSuggestions.d.ts +14 -0
- package/dist/apps/shared/components/mail/compose/protectedContactSuggestions.js +96 -0
- package/dist/apps/shared/components/mail/invite/inviteStore.js +11 -1
- package/dist/apps/shared/components/mail/pinnedSigners.d.ts +6 -0
- package/dist/apps/shared/components/mail/pinnedSigners.js +10 -1
- package/dist/apps/shared/components/mail/reading/conversationPrint.d.ts +2 -2
- package/dist/apps/shared/components/mail/reading/conversationPrint.js +3 -3
- package/dist/apps/shared/components/settings/KeyExportSetting.js +3 -1
- package/dist/apps/shared/components/settings/OpenPgpKeySetting.d.ts +13 -0
- package/dist/apps/shared/components/settings/OpenPgpKeySetting.js +143 -0
- package/dist/apps/shared/components/settings/ZeroAccessSetting.d.ts +2 -0
- package/dist/apps/shared/components/settings/ZeroAccessSetting.js +12 -3
- package/dist/apps/shared/components/settings/ZeroAccessTypes.d.ts +29 -0
- package/dist/apps/shared/components/settings/ZeroAccessTypes.js +107 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +3 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +3 -2
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +37 -4
- package/dist/apps/shared/mail/outbox/sendJob.js +287 -42
- package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +12 -0
- package/dist/apps/shared/mail/outbox/sendOutcomes.js +76 -20
- package/dist/apps/shared/search/LocalIndexLifecycle.js +55 -33
- package/dist/apps/shared/search/localIndexBuilder.d.ts +4 -1
- package/dist/apps/shared/search/localIndexBuilder.js +31 -20
- package/dist/apps/shared/search/localIndexRpcClient.d.ts +5 -39
- package/dist/apps/shared/search/localIndexRpcClient.js +126 -10
- package/dist/apps/shared/search/localIndexSchema.d.ts +14 -2
- package/dist/apps/shared/search/localIndexSchema.js +16 -2
- package/dist/apps/shared/search/localIndexTransport.d.ts +6 -3
- package/dist/apps/shared/search/localIndexWorker.d.ts +9 -0
- package/dist/apps/shared/search/localIndexWorker.js +22 -15
- package/dist/apps/shared/search/searchTier2.js +5 -0
- package/dist/apps/shared/search/zeroAccessIndexSync.d.ts +37 -6
- package/dist/apps/shared/search/zeroAccessIndexSync.js +199 -37
- package/dist/apps/shared/search/zeroAccessSyncState.d.ts +20 -0
- package/dist/apps/shared/search/zeroAccessSyncState.js +65 -0
- package/dist/apps/shared/zeroAccess/ProtectedMark.d.ts +7 -0
- package/dist/apps/shared/zeroAccess/ProtectedMark.js +10 -0
- package/dist/apps/shared/zeroAccess/useKeysLocked.d.ts +6 -0
- package/dist/apps/shared/zeroAccess/useKeysLocked.js +20 -0
- package/dist/apps/shared/zeroAccess/useOpenedRecord.d.ts +18 -0
- package/dist/apps/shared/zeroAccess/useOpenedRecord.js +66 -0
- package/dist/apps/www/_shell.d.ts +1 -1
- package/dist/apps/www/_shell.js +2 -2
- package/dist/apps/www/calendar/index.js +47 -5
- package/dist/apps/www/contacts/index.js +64 -11
- package/dist/apps/www/index.js +10 -1
- package/dist/apps/www/settings/encryption/index.js +14 -4
- package/dist/apps/www/settings/privacy/index.js +46 -5
- package/dist/apps/www/tasks/index.js +21 -7
- package/dist/lib/admin/domainsApi.d.ts +1 -1
- package/dist/lib/calendar/calendarApi.d.ts +46 -6
- package/dist/lib/calendar/calendarApi.js +110 -17
- package/dist/lib/calendar/calendarMutations.d.ts +20 -7
- package/dist/lib/calendar/calendarMutations.js +73 -14
- package/dist/lib/calendar/inviteApi.d.ts +17 -3
- package/dist/lib/calendar/inviteApi.js +61 -4
- package/dist/lib/calendar/protectedEvents.d.ts +31 -0
- package/dist/lib/calendar/protectedEvents.js +113 -0
- package/dist/lib/calendar/recurrence.d.ts +2 -0
- package/dist/lib/calendar/recurrence.js +16 -0
- package/dist/lib/calendar/sealedInvites.d.ts +6 -8
- package/dist/lib/calendar/sealedInvites.js +43 -6
- package/dist/lib/contacts/contactsApi.d.ts +29 -3
- package/dist/lib/contacts/contactsApi.js +23 -7
- package/dist/lib/contacts/useContactPhotoSrc.d.ts +5 -2
- package/dist/lib/contacts/useContactPhotoSrc.js +91 -11
- package/dist/lib/crypto/composeSecurity.d.ts +11 -3
- package/dist/lib/crypto/composeSecurity.js +13 -5
- package/dist/lib/crypto/keyExport.js +2 -2
- package/dist/lib/crypto/keyPairMatch.d.ts +9 -0
- package/dist/lib/crypto/keyPairMatch.js +65 -0
- package/dist/lib/crypto/keySession.d.ts +19 -2
- package/dist/lib/crypto/keySession.js +89 -13
- package/dist/lib/crypto/keyvaultApi.d.ts +41 -5
- package/dist/lib/crypto/keyvaultApi.js +13 -3
- package/dist/lib/crypto/meetingKeys.d.ts +25 -0
- package/dist/lib/crypto/meetingKeys.js +87 -0
- package/dist/lib/crypto/messageSecurity.d.ts +26 -2
- package/dist/lib/crypto/messageSecurity.js +122 -10
- package/dist/lib/crypto/openPgpKey.d.ts +39 -0
- package/dist/lib/crypto/openPgpKey.js +89 -0
- package/dist/lib/crypto/openpgp.d.ts +47 -0
- package/dist/lib/crypto/openpgp.js +88 -0
- package/dist/lib/crypto/pgpMessage.d.ts +51 -0
- package/dist/lib/crypto/pgpMessage.js +271 -0
- package/dist/lib/crypto/sharedMailboxKeys.d.ts +15 -3
- package/dist/lib/crypto/sharedMailboxKeys.js +102 -29
- package/dist/lib/crypto/smimeMessage.d.ts +21 -0
- package/dist/lib/crypto/smimeMessage.js +11 -7
- package/dist/lib/crypto/zeroAccessKey.js +5 -0
- package/dist/lib/mail/mailApi.d.ts +33 -0
- package/dist/lib/mail/mailboxImportApi.d.ts +10 -0
- package/dist/lib/mail/mailboxImportApi.js +15 -0
- package/dist/lib/search/zeroAccessIndexApi.d.ts +4 -0
- package/dist/lib/tasks/tasksApi.d.ts +11 -1
- package/dist/lib/tasks/tasksApi.js +10 -4
- package/dist/lib/util/clientPlatform.d.ts +14 -0
- package/dist/lib/util/clientPlatform.js +26 -0
- package/dist/lib/videoconf/protectedMeetings.d.ts +74 -0
- package/dist/lib/videoconf/protectedMeetings.js +181 -0
- package/dist/lib/videoconf/videoMeetingsApi.d.ts +25 -4
- package/dist/lib/videoconf/videoMeetingsApi.js +1 -1
- package/dist/lib/zeroAccess/sealedMoves.d.ts +41 -0
- package/dist/lib/zeroAccess/sealedMoves.js +63 -0
- package/dist/lib/zeroAccess/sealedRecords.d.ts +29 -0
- package/dist/lib/zeroAccess/sealedRecords.js +136 -0
- package/lib/admin/domainsApi.ts +1 -1
- package/lib/calendar/calendarApi.ts +140 -17
- package/lib/calendar/calendarMutations.ts +79 -12
- package/lib/calendar/inviteApi.ts +75 -5
- package/lib/calendar/protectedEvents.ts +143 -0
- package/lib/calendar/recurrence.ts +17 -0
- package/lib/calendar/sealedInvites.ts +47 -7
- package/lib/contacts/contactsApi.ts +43 -17
- package/lib/contacts/useContactPhotoSrc.ts +103 -13
- package/lib/crypto/composeSecurity.ts +20 -5
- package/lib/crypto/keyExport.ts +2 -2
- package/lib/crypto/keyPairMatch.ts +69 -0
- package/lib/crypto/keySession.ts +102 -12
- package/lib/crypto/keyvaultApi.ts +49 -7
- package/lib/crypto/meetingKeys.ts +118 -0
- package/lib/crypto/messageSecurity.ts +174 -13
- package/lib/crypto/openPgpKey.ts +118 -0
- package/lib/crypto/openpgp.ts +115 -0
- package/lib/crypto/pgpMessage.ts +336 -0
- package/lib/crypto/sharedMailboxKeys.ts +106 -28
- package/lib/crypto/smimeMessage.ts +11 -7
- package/lib/crypto/zeroAccessKey.ts +5 -0
- package/lib/mail/mailApi.ts +27 -0
- package/lib/mail/mailboxImportApi.ts +24 -0
- package/lib/search/zeroAccessIndexApi.ts +4 -0
- package/lib/tasks/tasksApi.ts +19 -5
- package/lib/util/clientPlatform.ts +50 -0
- package/lib/videoconf/protectedMeetings.ts +221 -0
- package/lib/videoconf/videoMeetingsApi.ts +15 -4
- package/lib/zeroAccess/sealedMoves.ts +79 -0
- package/lib/zeroAccess/sealedRecords.ts +153 -0
- package/package.json +12 -3
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
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"];
|
|
3
5
|
export interface ZeroAccessSettingProps {
|
|
4
6
|
mailbox: Mailbox;
|
|
5
7
|
/** The caller owns the mailbox and isn't impersonating: only then can the setting be changed (restapi drops anybody else's value). */
|
|
@@ -9,7 +9,11 @@ import { getMailbox, updateMailbox } from "../../../../lib/mail/mailApi.js";
|
|
|
9
9
|
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
10
10
|
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
11
11
|
import { notifyApiError } from "../../notifications/apiErrors.js";
|
|
12
|
+
import { useSettingsShell } from "./layout/SettingsShell.js";
|
|
12
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"];
|
|
13
17
|
/**
|
|
14
18
|
* "Enable zero-access encryption" for one mailbox (restapi `specs/search.md` §3): unencrypted mail is encrypted to the
|
|
15
19
|
* mailbox's zero-access key as it arrives and the server keeps no readable copy. On by default. Shown disabled, with the
|
|
@@ -18,6 +22,7 @@ import PolicyLockedNotice from "./PolicyLockedNotice.js";
|
|
|
18
22
|
*/
|
|
19
23
|
export default function ZeroAccessSetting({ mailbox, canManage }) {
|
|
20
24
|
const client = useApiClient();
|
|
25
|
+
const { patchMailbox } = useSettingsShell();
|
|
21
26
|
const [enabled, setEnabled] = useState(mailbox.zeroAccessEnabled !== false);
|
|
22
27
|
const [blocker, setBlocker] = useState(undefined);
|
|
23
28
|
const [saving, setSaving] = useState(false);
|
|
@@ -46,8 +51,9 @@ export default function ZeroAccessSetting({ mailbox, canManage }) {
|
|
|
46
51
|
setEnabled(next);
|
|
47
52
|
try {
|
|
48
53
|
const write = (version) => updateMailbox({ uid: mailbox.uid, version, zeroAccessEnabled: next }, client);
|
|
54
|
+
let saved;
|
|
49
55
|
try {
|
|
50
|
-
await write(mailbox.version);
|
|
56
|
+
saved = await write(mailbox.version);
|
|
51
57
|
}
|
|
52
58
|
catch (err) {
|
|
53
59
|
// Enrolling a key (which happens on unlock) moves the mailbox's version on, so the copy this page was given may be
|
|
@@ -55,8 +61,11 @@ export default function ZeroAccessSetting({ mailbox, canManage }) {
|
|
|
55
61
|
if (!(err instanceof ApiRequestError && err.status === 409)) {
|
|
56
62
|
throw err;
|
|
57
63
|
}
|
|
58
|
-
await write((await getMailbox(mailbox.uid, {}, client)).version);
|
|
64
|
+
saved = await write((await getMailbox(mailbox.uid, {}, client)).version);
|
|
59
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 });
|
|
60
69
|
}
|
|
61
70
|
catch (err) {
|
|
62
71
|
setEnabled(previous);
|
|
@@ -67,5 +76,5 @@ export default function ZeroAccessSetting({ mailbox, canManage }) {
|
|
|
67
76
|
}
|
|
68
77
|
}
|
|
69
78
|
const locked = blocker !== null;
|
|
70
|
-
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." })] })] }), 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." }))] }));
|
|
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." }))] }));
|
|
71
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;
|
|
@@ -0,0 +1,107 @@
|
|
|
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 { getMailbox, updateMailbox } from "../../../../lib/mail/mailApi.js";
|
|
8
|
+
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
9
|
+
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
10
|
+
import { notifyApiError } from "../../notifications/apiErrors.js";
|
|
11
|
+
import { useSettingsShell } from "./layout/SettingsShell.js";
|
|
12
|
+
/** What each data type's protection covers, what stays readable and why, and what it breaks on a phone or Outlook - the copy restapi
|
|
13
|
+
* `specs/search.md` §3 *What It Covers* requires settings to state plainly. */
|
|
14
|
+
export const ZERO_ACCESS_TYPE_INFO = {
|
|
15
|
+
mail: {
|
|
16
|
+
label: "Mail",
|
|
17
|
+
encrypted: "Message bodies, subjects, previews and attachments",
|
|
18
|
+
readable: "Sender, recipients, dates, size, flags and folder - to deliver, thread and search your mail",
|
|
19
|
+
onDevices: "can't open protected mail until you install your exported keys on it (Export keys below)",
|
|
20
|
+
},
|
|
21
|
+
calendar: {
|
|
22
|
+
label: "Calendar",
|
|
23
|
+
encrypted: "Event titles, locations and descriptions",
|
|
24
|
+
readable: "Times, repeats, time zone, attendees, responses and busy status - for free/busy, invitations and reminders",
|
|
25
|
+
onDevices: "shows protected events only as busy blocks named “Protected event”, and attendees aren't told when it changes a meeting (a phone) or sends one (Outlook) - send those from RapidMX",
|
|
26
|
+
},
|
|
27
|
+
contacts: {
|
|
28
|
+
label: "Contacts",
|
|
29
|
+
encrypted: "Names, company, job title, phone numbers, postal addresses, notes, categories and photos",
|
|
30
|
+
readable: "Email addresses - the server needs them to deliver mail and to find each contact's encryption keys",
|
|
31
|
+
onDevices: "shows protected contacts by their email address only",
|
|
32
|
+
},
|
|
33
|
+
tasks: {
|
|
34
|
+
label: "Tasks",
|
|
35
|
+
encrypted: "Task titles and notes",
|
|
36
|
+
readable: "Due dates, reminders, priority, completion and list",
|
|
37
|
+
onDevices: "shows protected tasks as “Protected task”",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
/** Whether `type` is included: the owner's choice, else (undecided) mail on and the rest off - restapi's `zeroAccessTypeIncluded()`. */
|
|
41
|
+
export function typeIncluded(types, type) {
|
|
42
|
+
const chosen = types?.[type];
|
|
43
|
+
return typeof chosen === "boolean" ? chosen : type === "mail";
|
|
44
|
+
}
|
|
45
|
+
/** The names of the owner's ActiveSync devices that sync `type`, and Outlook when it syncs the mailbox. */
|
|
46
|
+
export function devicesSyncing(mailbox, type) {
|
|
47
|
+
const names = (mailbox.syncClients?.activesync?.devices ?? []).filter((device) => device.types.includes(type)).map((device) => device.model || "A phone");
|
|
48
|
+
return [...new Set(names), ...(mailbox.syncClients?.mapi ? ["Outlook"] : [])];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* "What to protect": one switch per data type zero-access encryption covers, each saying what it encrypts and - for a type one of the
|
|
52
|
+
* owner's ActiveSync devices or Outlook syncs - plainly what that device will no longer show (restapi `specs/search.md` §3 *What It
|
|
53
|
+
* Covers*). Then the table of what stays readable to the server and why.
|
|
54
|
+
*/
|
|
55
|
+
export default function ZeroAccessTypes({ mailbox, types, canManage, disabled = false }) {
|
|
56
|
+
const client = useApiClient();
|
|
57
|
+
const { patchMailbox } = useSettingsShell();
|
|
58
|
+
const [chosen, setChosen] = useState(mailbox.zeroAccessTypes ?? {});
|
|
59
|
+
const [saving, setSaving] = useState(false);
|
|
60
|
+
// What the server saved, once the mailbox has it (turning zero-access on decides the undecided types there): shown instead of what this
|
|
61
|
+
// was mounted with - set while rendering, so the stale switches are never drawn.
|
|
62
|
+
const storedTypes = JSON.stringify(mailbox.zeroAccessTypes ?? {});
|
|
63
|
+
const [shownTypes, setShownTypes] = useState(storedTypes);
|
|
64
|
+
if (shownTypes !== storedTypes) {
|
|
65
|
+
setShownTypes(storedTypes);
|
|
66
|
+
setChosen(JSON.parse(storedTypes));
|
|
67
|
+
}
|
|
68
|
+
async function save(type, on) {
|
|
69
|
+
const previous = chosen;
|
|
70
|
+
const next = { ...chosen, [type]: on };
|
|
71
|
+
setChosen(next);
|
|
72
|
+
setSaving(true);
|
|
73
|
+
try {
|
|
74
|
+
const write = (version) => updateMailbox({ uid: mailbox.uid, version, zeroAccessTypes: { [type]: on } }, client);
|
|
75
|
+
let saved;
|
|
76
|
+
try {
|
|
77
|
+
saved = await write(mailbox.version);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
// The server records devices on the mailbox, moving its version on: read it again and retry once.
|
|
81
|
+
if (!(err instanceof ApiRequestError && err.status === 409)) {
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
saved = await write((await getMailbox(mailbox.uid, {}, client)).version);
|
|
85
|
+
}
|
|
86
|
+
// Kept by the shell, so turning zero-access off and on again (which mounts this anew) shows what was saved: the server's merge of
|
|
87
|
+
// this choice into the stored one.
|
|
88
|
+
const merged = saved.zeroAccessTypes ?? next;
|
|
89
|
+
setChosen(merged);
|
|
90
|
+
patchMailbox?.(mailbox.uid, { zeroAccessTypes: merged, version: saved.version });
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
setChosen(previous);
|
|
94
|
+
notifyApiError(err, "Couldn't save what zero-access encryption protects");
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
setSaving(false);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return (_jsxs("div", { className: "mt-3", children: [_jsx("h3", { className: "text-sm font-semibold mb-1", children: "What to protect" }), _jsx("ul", { className: "flex flex-col gap-2", children: types.map((type) => {
|
|
101
|
+
const info = ZERO_ACCESS_TYPE_INFO[type];
|
|
102
|
+
const devices = devicesSyncing(mailbox, type);
|
|
103
|
+
const on = typeIncluded(chosen, type);
|
|
104
|
+
const keyInstalled = type === "mail" && !!mailbox.zeroAccessKeysExportedAt;
|
|
105
|
+
return (_jsx("li", { children: _jsxs("label", { className: "flex items-start gap-2 text-sm", children: [_jsx("input", { type: "checkbox", className: "mt-1", checked: on, disabled: !canManage || saving || disabled, onChange: (e) => void save(type, e.target.checked) }), _jsxs("span", { children: [_jsx("span", { className: "font-semibold block", children: info.label }), _jsxs("span", { className: "text-xs text-text-muted block", children: [info.encrypted, "."] }), devices.length > 0 && !keyInstalled && (_jsxs("span", { role: on ? "alert" : undefined, className: `text-xs block ${on ? "text-danger" : "text-text-muted"}`, children: [on ? "Breaks: " : "Left off because ", devices.join(", "), " ", devices.length === 1 ? "syncs" : "sync", " this - ", on ? "it " : "protected, it ", info.onDevices, "."] }))] })] }) }, type));
|
|
106
|
+
}) }), _jsxs("details", { className: "mt-3 text-sm", children: [_jsx("summary", { className: "cursor-pointer font-semibold", children: "What\u2019s encrypted, and what isn\u2019t" }), _jsxs("table", { className: "mt-2 w-full text-xs border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { className: "text-left", children: [_jsx("th", { className: "py-1 pr-2", children: "Type" }), _jsx("th", { className: "py-1 pr-2", children: "Encrypted" }), _jsx("th", { className: "py-1", children: "Readable by the server, and why" })] }) }), _jsx("tbody", { children: types.map((type) => (_jsxs("tr", { className: "border-t border-border align-top", children: [_jsx("td", { className: "py-1 pr-2 font-semibold", children: ZERO_ACCESS_TYPE_INFO[type].label }), _jsx("td", { className: "py-1 pr-2", children: typeIncluded(chosen, type) ? ZERO_ACCESS_TYPE_INFO[type].encrypted : "Nothing - not protected" }), _jsx("td", { className: "py-1", children: ZERO_ACCESS_TYPE_INFO[type].readable })] }, type))) })] }), _jsx("p", { className: "mt-2 text-xs text-text-muted", children: "For every type, the server can also see folder and list names, how many items there are and when they change. Zero-access encryption protects what is stored: the server sees what you save while it saves it, then keeps only the encrypted copy." })] })] }));
|
|
107
|
+
}
|
|
@@ -30,6 +30,9 @@ export interface SettingsShellContextValue {
|
|
|
30
30
|
* every settings section so far (Automatic Replies) edits mailbox-scoped fields. */
|
|
31
31
|
mailboxUid?: string;
|
|
32
32
|
mailboxes: Mailbox[];
|
|
33
|
+
/** Merges what a section just saved of a mailbox (its new `version` included) into the shell's copy, so a section mounted again for
|
|
34
|
+
* it - after the mailbox switcher, say - starts from what was saved rather than from the list loaded once. */
|
|
35
|
+
patchMailbox?: (mailboxUid: string, patch: Partial<Mailbox>) => void;
|
|
33
36
|
}
|
|
34
37
|
/** Reads the mailbox Settings is currently scoped to, as resolved by the enclosing `SettingsShell`. */
|
|
35
38
|
export declare function useSettingsShell(): SettingsShellContextValue;
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
7
7
|
import { HiOutlineArrowUturnLeft, HiOutlineBars3, HiOutlineEnvelopeOpen, HiOutlineFunnel, HiOutlineLockClosed, HiOutlineNoSymbol, HiOutlinePaintBrush, HiOutlinePencilSquare, HiOutlineShieldCheck, HiOutlineTag, HiOutlineUserCircle, HiOutlineUsers, } from "react-icons/hi2";
|
|
8
8
|
import { ApiRequestError } from "../../../../../lib/util/api.js";
|
|
9
9
|
import Drawer from "../../../../../lib/components/overlays/Drawer.js";
|
|
@@ -89,7 +89,8 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
89
89
|
}, [userUid]);
|
|
90
90
|
const mailboxUid = (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
|
|
91
91
|
primaryMailboxUid(mailboxes, userUid);
|
|
92
|
-
const
|
|
92
|
+
const patchMailbox = useCallback((uid, patch) => setMailboxes((list) => list.map((mb) => (mb.uid === uid ? { ...mb, ...patch } : mb))), []);
|
|
93
|
+
const contextValue = useMemo(() => ({ mailboxUid, mailboxes, patchMailbox }), [mailboxUid, mailboxes, patchMailbox]);
|
|
93
94
|
// A full-screen takeover, not nested inside the rest of the app's chrome — same as ContactsShell/
|
|
94
95
|
// TasksShell, there's nothing else for a mailbox-less caller to configure here yet.
|
|
95
96
|
if (userUid && status === "ready" && !mailboxUid) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ApiClient } from "../../../../lib/util/api.js";
|
|
2
2
|
import { Attachment, ComposeRecipientInput, DraftThreading, Mailbox, Message } from "../../../../lib/mail/mailApi.js";
|
|
3
3
|
import { RecipientEncryptionStatus } from "../../../../lib/crypto/composeSecurity.js";
|
|
4
|
+
import { type UnlockedKeys } from "../../../../lib/crypto/keySession.js";
|
|
4
5
|
import { EncryptionPolicy } from "../../../../lib/crypto/keyvaultApi.js";
|
|
5
6
|
import { SendDecisionInput } from "./sendDecision.js";
|
|
6
7
|
import { OutboxCountTracker } from "./sendState.js";
|
|
@@ -39,20 +40,52 @@ export interface SendRequest {
|
|
|
39
40
|
* show the message as soon as it is sent (see `outgoingReplies.ts`). Absent for a new message, and for a draft opened again, which the draft's own
|
|
40
41
|
* `inReplyTo`/`references` stand in for. */
|
|
41
42
|
threading?: DraftThreading;
|
|
43
|
+
/** The copies of this message (`assemble()`: one per encryption format) already built but not handed over yet, after an attempt that
|
|
44
|
+
* failed part-way: a retry hands over just these - never one that already went, and without building them again. */
|
|
45
|
+
pendingCopies?: Message[];
|
|
46
|
+
/** Some copies already went (`pendingCopies` holds only the rest): every later failure says so, and never offers "Open draft", which
|
|
47
|
+
* would send those recipients a second copy - kept across any number of retries. */
|
|
48
|
+
partial?: boolean;
|
|
49
|
+
/** The uids of every copy of this message, when it is more than one (one per encryption format, each delivered only to its own
|
|
50
|
+
* recipients): the request is retained under each, and a failure the server reports later for one of them offers no "Open draft",
|
|
51
|
+
* which would send the other copy's recipients a second message. */
|
|
52
|
+
copyUids?: string[];
|
|
53
|
+
/** What has become of each of those copies, by uid: one map shared by every request made from this one (a retry's, the one retained), so
|
|
54
|
+
* how any copy went is known to all of them. */
|
|
55
|
+
copyFates?: Map<string, CopyFate>;
|
|
42
56
|
}
|
|
57
|
+
/** One copy of a message sent as several (`SendRequest.copyFates`): `unsent` - not handed over (or its hand-over failed), `pending` - handed
|
|
58
|
+
* over, how it went still to be reported, `sent`, or `failed` - the server reported it couldn't relay it. */
|
|
59
|
+
export type CopyFate = "unsent" | "pending" | "sent" | "failed";
|
|
43
60
|
/** How long a background send waits for the mailbox / encryption policy it did not have, before it goes ahead treating them as unknown. Nobody waits for it. */
|
|
44
61
|
export declare const CONTEXT_GRACE_MS = 4000;
|
|
45
62
|
export declare function registerOutboxCounter(tracker: (mailboxUid: string) => OutboxCountTracker, onQueued: (mailboxUid: string) => void): () => void;
|
|
46
|
-
/** The request a message was queued from, while this tab still holds it: what lets "Open draft" and "Retry" work on a failure the server
|
|
63
|
+
/** The request a message was queued from, while this tab still holds it: what lets "Open draft" and "Retry" work on a failure the server
|
|
64
|
+
* reports later. A message sent as several copies (`SendRequest.copyUids`) is held under each copy still to go, from just before it is handed
|
|
65
|
+
* over - how the first went can be reported while the next is still being handed over. */
|
|
47
66
|
export declare function retainedRequest(draftUid: string): SendRequest | undefined;
|
|
48
|
-
/** Drops the request a message was queued from, and the pictures remembered as attached to its draft: it has gone, so
|
|
49
|
-
|
|
67
|
+
/** Drops the request a message (or one of its copies) was queued from, and the pictures remembered as attached to its draft: it has gone, so
|
|
68
|
+
* neither is wanted again. Whether the message has now gone in full - the last of its copies to go. */
|
|
69
|
+
export declare function forgetRetainedRequest(draftUid: string): boolean;
|
|
70
|
+
/** Records that the server couldn't relay copy `uid` of the message `request` sent as several copies (nothing for a message sent as one). */
|
|
71
|
+
export declare function copyFailed(request: SendRequest | undefined, uid: string): void;
|
|
72
|
+
/** Copy `uid` of the message `request` sent as several copies is handed over again (a Retry): how it goes is to be reported once more, and
|
|
73
|
+
* finds the request held under it. Nothing for a message sent as one. */
|
|
74
|
+
export declare function copyRetried(request: SendRequest | undefined, uid: string): void;
|
|
75
|
+
/** Every copy of the message `request` sent as several, when none of them has gone nor may still go - each failed, or was never handed over:
|
|
76
|
+
* nobody has the message. `undefined` while one has gone or may, and for a message sent as one. */
|
|
77
|
+
export declare function unsentCopies(request: SendRequest | undefined): string[] | undefined;
|
|
78
|
+
/** Stops holding a message sent as several copies under any of them: it is being written again. */
|
|
79
|
+
export declare function forgetCopies(fates: Map<string, CopyFate>): void;
|
|
50
80
|
/** The sender's signing/encryption key state right now, read from the tab's key session (they can lock mid-send). */
|
|
51
81
|
export declare function readSendKeys(mailboxUid: string, signEnabled: boolean, mailbox: Mailbox | undefined): {
|
|
52
|
-
unlocked:
|
|
82
|
+
unlocked: UnlockedKeys | undefined;
|
|
53
83
|
canSign: boolean;
|
|
54
84
|
canEncryptSelf: boolean;
|
|
55
85
|
};
|
|
86
|
+
/** Whether the sender can make an OpenPGP copy (it is encrypted to them too, with their own OpenPGP key) - or can't be told yet, the
|
|
87
|
+
* mailbox being locked: `decideSend()` then asks for the unlock rather than settling for another format, or none. */
|
|
88
|
+
export declare function senderCanUseOpenPgp(unlocked: UnlockedKeys | undefined): boolean;
|
|
56
89
|
/** Whether this sender could encrypt at all: an encryption key unlocked here, enrolled on the mailbox, or offered earlier this session. */
|
|
57
90
|
export declare function senderHasEncryptionKey(request: Pick<SendRequest, "mailbox" | "offeredEncrypt">, canEncryptSelf: boolean): boolean;
|
|
58
91
|
/** The input `decideSend()` reads, from a request and what has been looked up. */
|