@rapidmx/web-client 0.36.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/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
|
@@ -4,8 +4,34 @@
|
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
/** Pure helpers behind `useCalendarReminders.ts` - kept apart from the hook so the push-payload parsing, the snooze
|
|
6
6
|
* arithmetic and the pop-up's wording are each testable without a React tree or a socket. */
|
|
7
|
+
import { getCalendarEvent } from "../../../lib/calendar/calendarApi.js";
|
|
8
|
+
import { openSealedEvent } from "./sealedEvents.js";
|
|
7
9
|
/** How long "Snooze" waits before the alarm comes back. */
|
|
8
10
|
export const SNOOZE_MS = 5 * 60000;
|
|
11
|
+
/** `notice` with a protected event's real title and location, read from the event itself (opened with the mailbox's keys); `notice` as it
|
|
12
|
+
* came when the event can't be read or opened - the alarm still goes off, with the placeholder. Never rejects. */
|
|
13
|
+
export async function openedReminder(notice) {
|
|
14
|
+
if (!notice.protected) {
|
|
15
|
+
return notice;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
// `getCalendarEvent()` opens sealed fields; an event made from a zero-access invitation keeps its content in `sealedContent`.
|
|
19
|
+
const event = await openSealedEvent(await getCalendarEvent(notice.eventUid));
|
|
20
|
+
return { ...notice, title: event.title, location: event.location ?? null, sealedTitle: notice.title, sealedLocation: notice.location ?? null };
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return notice;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** `notice` as the push carried it, when `openedReminder()` opened it - for an alarm still shown, or snoozed, when the mailbox locks: its real
|
|
27
|
+
* title and location go with the keys that opened them. `openedReminder()` opens it again (it is still `protected`). */
|
|
28
|
+
export function sealedReminder(notice) {
|
|
29
|
+
if (notice.sealedTitle === undefined) {
|
|
30
|
+
return notice;
|
|
31
|
+
}
|
|
32
|
+
const { sealedTitle, sealedLocation, ...rest } = notice;
|
|
33
|
+
return { ...rest, title: sealedTitle, location: sealedLocation ?? null };
|
|
34
|
+
}
|
|
9
35
|
function isReminderNotice(data) {
|
|
10
36
|
const notice = data;
|
|
11
37
|
return (!!notice &&
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* decryption code is loaded only when a sealed event is actually seen.
|
|
6
6
|
*/
|
|
7
7
|
import type { CalendarEvent } from "../../../lib/calendar/calendarApi.js";
|
|
8
|
-
/** Forgets every opened event (sign-out, or the test seam). */
|
|
8
|
+
/** Forgets every opened event (a mailbox locking, sign-out, or the test seam). */
|
|
9
9
|
export declare function clearSealedEvents(): void;
|
|
10
|
-
/** `events` with every sealed one this device can open shown with its real title, location and description
|
|
10
|
+
/** `events` with every sealed one this device can open shown with its real title, location and description (and marked opened,
|
|
11
|
+
* `isSealedContentOpened()`). Never rejects. */
|
|
11
12
|
export declare function openSealedEvents(events: CalendarEvent[]): Promise<CalendarEvent[]>;
|
|
12
13
|
/** `openSealedEvents()` for one event. */
|
|
13
14
|
export declare function openSealedEvent(event: CalendarEvent): Promise<CalendarEvent>;
|
|
@@ -1,32 +1,55 @@
|
|
|
1
|
-
import { getUnlockedKeys } from "../../../lib/crypto/keySession.js";
|
|
1
|
+
import { getUnlockedKeys, subscribeKeySession } from "../../../lib/crypto/keySession.js";
|
|
2
2
|
import { fromBase64 } from "../../../lib/crypto/encoding.js";
|
|
3
|
+
import { markSealedContentOpened } from "../../../lib/calendar/protectedEvents.js";
|
|
3
4
|
import { onSigningOut } from "../components/mail/compose/composeFlushRegistry.js";
|
|
4
5
|
/** Opened fields by sealed text - the same sealed content always opens to the same fields. Cleared with `clearSealedEvents()`. */
|
|
5
6
|
const opened = new Map();
|
|
6
|
-
/** Forgets every opened event (sign-out, or the test seam). */
|
|
7
|
+
/** Forgets every opened event (a mailbox locking, sign-out, or the test seam). */
|
|
7
8
|
export function clearSealedEvents() {
|
|
8
9
|
opened.clear();
|
|
9
10
|
}
|
|
11
|
+
let watchingKeys = false;
|
|
12
|
+
/** Opened fields go when keys are locked, so nothing opened outlives its key session. Subscribed when the first sealed event is seen,
|
|
13
|
+
* not on import. */
|
|
14
|
+
function forgetOnLock() {
|
|
15
|
+
if (watchingKeys) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
watchingKeys = true;
|
|
19
|
+
subscribeKeySession((event) => {
|
|
20
|
+
if (event.state === "locked") {
|
|
21
|
+
clearSealedEvents();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
10
25
|
async function openFields(event) {
|
|
11
26
|
const sealed = event.sealedContent.sealed;
|
|
12
|
-
|
|
13
|
-
if (known) {
|
|
14
|
-
return known;
|
|
15
|
-
}
|
|
27
|
+
// Checked before the cache: a locked mailbox's events show their placeholders, whatever was opened before.
|
|
16
28
|
const keys = getUnlockedKeys(event.mailboxUid)?.zeroAccessKeys;
|
|
17
29
|
if (!keys?.length) {
|
|
18
30
|
return undefined;
|
|
19
31
|
}
|
|
32
|
+
const known = opened.get(sealed);
|
|
33
|
+
if (known) {
|
|
34
|
+
return known;
|
|
35
|
+
}
|
|
20
36
|
try {
|
|
21
37
|
const { decryptEnvelopedDataWithKeys } = await import("../../../lib/crypto/smime.js");
|
|
22
38
|
const fields = JSON.parse(new TextDecoder().decode(await decryptEnvelopedDataWithKeys(fromBase64(sealed), keys)));
|
|
23
39
|
const text = (value) => (typeof value === "string" ? value : undefined);
|
|
24
40
|
const result = {
|
|
25
|
-
title:
|
|
41
|
+
// An empty sealed title is no title: a newer revision without SUMMARY over a row that keeps its title sealed field by field
|
|
42
|
+
// (`sealedFields.title`) is sealed with an empty one, and the title opened from that copy (or the row's own) shows instead.
|
|
43
|
+
// Left out here, not copied from `event`, as the cache is shared by every row with this sealed content.
|
|
44
|
+
...(text(fields.title) ? { title: text(fields.title) } : {}),
|
|
26
45
|
location: text(fields.location),
|
|
27
46
|
description: text(fields.description) ?? null,
|
|
28
47
|
descriptionHtml: text(fields.descriptionHtml) ?? null,
|
|
29
48
|
};
|
|
49
|
+
// The mailbox locked while this was opened: the lock's clear has already run, so it is neither kept nor shown.
|
|
50
|
+
if (!getUnlockedKeys(event.mailboxUid)) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
30
53
|
opened.set(sealed, result);
|
|
31
54
|
return result;
|
|
32
55
|
}
|
|
@@ -34,17 +57,25 @@ async function openFields(event) {
|
|
|
34
57
|
return undefined;
|
|
35
58
|
}
|
|
36
59
|
}
|
|
37
|
-
/** `events` with every sealed one this device can open shown with its real title, location and description
|
|
60
|
+
/** `events` with every sealed one this device can open shown with its real title, location and description (and marked opened,
|
|
61
|
+
* `isSealedContentOpened()`). Never rejects. */
|
|
38
62
|
export async function openSealedEvents(events) {
|
|
39
63
|
if (!events.some((event) => event.sealedContent)) {
|
|
40
64
|
return events;
|
|
41
65
|
}
|
|
66
|
+
forgetOnLock();
|
|
42
67
|
return Promise.all(events.map(async (event) => {
|
|
43
68
|
if (!event.sealedContent) {
|
|
44
69
|
return event;
|
|
45
70
|
}
|
|
46
71
|
const fields = await openFields(event);
|
|
47
|
-
|
|
72
|
+
if (!fields) {
|
|
73
|
+
return event;
|
|
74
|
+
}
|
|
75
|
+
// Marked, so a reply or change request names it by its real title (`invitationContentOf()`). An empty sealed title with no
|
|
76
|
+
// per-field copy to show instead (an invitation without SUMMARY) is an untitled event: opened, it never shows the row's `[...]`.
|
|
77
|
+
const untitled = fields.title === undefined && !event.sealedFields?.title ? { title: "" } : {};
|
|
78
|
+
return markSealedContentOpened({ ...event, ...fields, ...untitled });
|
|
48
79
|
}));
|
|
49
80
|
}
|
|
50
81
|
/** `openSealedEvents()` for one event. */
|
|
@@ -28,6 +28,7 @@ export interface CalendarAlarms {
|
|
|
28
28
|
* with the bell again - after exactly `SNOOZE_MS`. A snooze is this tab's own `setTimeout`, cleared on unmount or sign-out; like the alarm itself (the
|
|
29
29
|
* push event is never replayed), it does not survive the tab closing.
|
|
30
30
|
*
|
|
31
|
-
* With pop-ups turned off (`getNotificationsEnabled()`) no dialog is shown and nothing is heard: the alarm is only recorded in "Recent notifications"
|
|
31
|
+
* With pop-ups turned off (`getNotificationsEnabled()`) no dialog is shown and nothing is heard: the alarm is only recorded in "Recent notifications"
|
|
32
|
+
* - a protected event's under its placeholder title, never the opened one (the history is kept in `sessionStorage`, past a lock).
|
|
32
33
|
*/
|
|
33
34
|
export declare function useCalendarReminders({ userUid, enabled }: UseCalendarRemindersOptions): CalendarAlarms;
|
|
@@ -7,7 +7,8 @@ import { getPushClient } from "../../../lib/mail/pushClient.js";
|
|
|
7
7
|
import { getNotificationsEnabled } from "../notifications/preferences.js";
|
|
8
8
|
import { playNotificationSound } from "../notifications/sounds.js";
|
|
9
9
|
import { notify } from "../notifications/store.js";
|
|
10
|
-
import {
|
|
10
|
+
import { useKeysLocked } from "../zeroAccess/useKeysLocked.js";
|
|
11
|
+
import { SNOOZE_MS, calendarReminderOf, openedReminder, reminderMessage, reminderNotificationId, sealedReminder } from "./calendarReminders.js";
|
|
11
12
|
/**
|
|
12
13
|
* Calendar alarms. restapi's `CalendarReminderJob` fires a `"CalendarEvent"`/`"reminder"` push event (`{ eventUid, title, startDate, location }`)
|
|
13
14
|
* when an event's `reminderMinutesBeforeStart` comes due, over the same shared push connection Mail already keeps open (`getPushClient()`) -
|
|
@@ -20,18 +21,25 @@ import { SNOOZE_MS, calendarReminderOf, reminderMessage, reminderNotificationId
|
|
|
20
21
|
* with the bell again - after exactly `SNOOZE_MS`. A snooze is this tab's own `setTimeout`, cleared on unmount or sign-out; like the alarm itself (the
|
|
21
22
|
* push event is never replayed), it does not survive the tab closing.
|
|
22
23
|
*
|
|
23
|
-
* With pop-ups turned off (`getNotificationsEnabled()`) no dialog is shown and nothing is heard: the alarm is only recorded in "Recent notifications"
|
|
24
|
+
* With pop-ups turned off (`getNotificationsEnabled()`) no dialog is shown and nothing is heard: the alarm is only recorded in "Recent notifications"
|
|
25
|
+
* - a protected event's under its placeholder title, never the opened one (the history is kept in `sessionStorage`, past a lock).
|
|
24
26
|
*/
|
|
25
27
|
export function useCalendarReminders({ userUid, enabled }) {
|
|
26
28
|
const [queue, setQueue] = useState([]);
|
|
27
29
|
// The queue as of the last render, for the callbacks below to read (a state updater must not start a timer: React may run it twice).
|
|
28
30
|
const queueRef = useRef(queue);
|
|
29
31
|
queueRef.current = queue;
|
|
30
|
-
|
|
32
|
+
// Each snoozed alarm by its timer: what it shows when it comes back (a lock meanwhile puts back its placeholders).
|
|
33
|
+
const timers = useRef(new Map());
|
|
31
34
|
const present = useCallback((notice) => {
|
|
32
35
|
if (!getNotificationsEnabled()) {
|
|
33
|
-
// Off: only the history keeps it.
|
|
34
|
-
notify({
|
|
36
|
+
// Off: only the history keeps it - with a protected event's placeholder title, since the history is stored in sessionStorage.
|
|
37
|
+
notify({
|
|
38
|
+
id: reminderNotificationId(notice.eventUid, notice.startDate),
|
|
39
|
+
kind: "calendar",
|
|
40
|
+
title: notice.sealedTitle ?? notice.title,
|
|
41
|
+
message: reminderMessage(notice.startDate),
|
|
42
|
+
});
|
|
35
43
|
return;
|
|
36
44
|
}
|
|
37
45
|
const id = reminderNotificationId(notice.eventUid, notice.startDate);
|
|
@@ -45,28 +53,38 @@ export function useCalendarReminders({ userUid, enabled }) {
|
|
|
45
53
|
const offEvent = getPushClient().onEvent((event) => {
|
|
46
54
|
const notice = calendarReminderOf(event);
|
|
47
55
|
if (notice) {
|
|
48
|
-
|
|
56
|
+
void openedReminder(notice).then(present);
|
|
49
57
|
}
|
|
50
58
|
});
|
|
51
59
|
const pending = timers.current;
|
|
52
60
|
return () => {
|
|
53
61
|
offEvent();
|
|
54
|
-
for (const timer of pending) {
|
|
62
|
+
for (const timer of pending.keys()) {
|
|
55
63
|
clearTimeout(timer);
|
|
56
64
|
}
|
|
57
65
|
pending.clear();
|
|
58
66
|
setQueue([]);
|
|
59
67
|
};
|
|
60
68
|
}, [enabled, userUid, present]);
|
|
69
|
+
// A protected event's alarm shows its title and location opened with the mailbox's keys: on a lock - a notice doesn't say which mailbox,
|
|
70
|
+
// so any - the alarms shown or snoozed go back to the placeholders the push carried.
|
|
71
|
+
useKeysLocked(() => {
|
|
72
|
+
setQueue((current) => current.map(sealedReminder));
|
|
73
|
+
for (const [timer, notice] of timers.current) {
|
|
74
|
+
timers.current.set(timer, sealedReminder(notice));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
61
77
|
const dismiss = useCallback(() => setQueue((current) => current.slice(1)), []);
|
|
62
78
|
const snooze = useCallback(() => {
|
|
63
79
|
const first = queueRef.current[0];
|
|
64
80
|
if (first) {
|
|
65
81
|
const timer = setTimeout(() => {
|
|
82
|
+
const notice = timers.current.get(timer);
|
|
66
83
|
timers.current.delete(timer);
|
|
67
|
-
|
|
84
|
+
// Put back to its placeholders by a lock: opened again if the mailbox is unlocked now.
|
|
85
|
+
void (notice.protected && notice.sealedTitle === undefined ? openedReminder(notice) : Promise.resolve(notice)).then(present);
|
|
68
86
|
}, SNOOZE_MS);
|
|
69
|
-
timers.current.
|
|
87
|
+
timers.current.set(timer, first);
|
|
70
88
|
}
|
|
71
89
|
setQueue((current) => current.slice(1));
|
|
72
90
|
}, [present]);
|
|
@@ -15,6 +15,7 @@ const RECORD_TYPE_LABELS = {
|
|
|
15
15
|
spf: "SPF",
|
|
16
16
|
dkim: "DKIM",
|
|
17
17
|
dmarc: "DMARC",
|
|
18
|
+
openpgpkey_cname: "OpenPGP keys (CNAME)",
|
|
18
19
|
autodiscover_cname: "Autodiscover (CNAME)",
|
|
19
20
|
autodiscover_srv: "Autodiscover (SRV)",
|
|
20
21
|
};
|
|
@@ -25,12 +26,14 @@ const RECORD_TYPE_NAMES = {
|
|
|
25
26
|
spf: "SPF",
|
|
26
27
|
dkim: "DKIM",
|
|
27
28
|
dmarc: "DMARC",
|
|
29
|
+
openpgpkey_cname: "OpenPGP keys CNAME",
|
|
28
30
|
autodiscover_cname: "Autodiscover CNAME",
|
|
29
31
|
autodiscover_srv: "Autodiscover SRV",
|
|
30
32
|
};
|
|
31
33
|
/** A one-line "why this matters", shown under a checklist row's label - only for record types unfamiliar
|
|
32
34
|
* enough to need one; the well-known mail records (ownership/MX/SPF/DKIM/DMARC) don't. */
|
|
33
35
|
const RECORD_TYPE_HELP = {
|
|
36
|
+
openpgpkey_cname: "So Proton Mail and other OpenPGP apps can find your users' OpenPGP keys (Web Key Directory) and send them encrypted mail. This hostname also needs to be covered by this server's TLS certificate.",
|
|
34
37
|
autodiscover_cname: "So EAS and Outlook clients can find this server automatically from just an email address. This hostname also needs to be covered by this server's TLS certificate.",
|
|
35
38
|
autodiscover_srv: "Same purpose as the CNAME above, for clients that check DNS SRV instead - and, unlike the CNAME, needs no extra TLS certificate, since it points at a hostname already covered. Prefer this one when a second certificate is hard to obtain.",
|
|
36
39
|
};
|
|
@@ -38,7 +38,11 @@ export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange,
|
|
|
38
38
|
const [savedValues, setSavedValues] = useState(policy);
|
|
39
39
|
// A server without zero-access encryption sends no `allowZeroAccess`; the checkbox is then not shown at all.
|
|
40
40
|
const zeroAccessSupported = policy.allowZeroAccess !== undefined;
|
|
41
|
-
|
|
41
|
+
// Likewise `allowOpenPgp`, from a server without OpenPGP support.
|
|
42
|
+
const openPgpSupported = policy.allowOpenPgp !== undefined;
|
|
43
|
+
const dirty = TIERS.some((tier) => values[tier.key] !== savedValues[tier.key]) ||
|
|
44
|
+
(zeroAccessSupported && values.allowZeroAccess !== savedValues.allowZeroAccess) ||
|
|
45
|
+
(openPgpSupported && values.allowOpenPgp !== savedValues.allowOpenPgp);
|
|
42
46
|
useEffect(() => onDirtyChange?.(dirty), [dirty]);
|
|
43
47
|
// Tiers this save would newly set to "Never encrypt", which switches encryption off for everyone's mail of that kind: confirmed first.
|
|
44
48
|
const [confirmingProhibit, setConfirmingProhibit] = useState(null);
|
|
@@ -86,5 +90,8 @@ export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange,
|
|
|
86
90
|
}, children: STATES.map((state) => (_jsx("option", { value: state.value, children: state.label }, state.value))) }), _jsx("span", { className: "text-xs text-text-muted", children: tier.help })] }, tier.key))), zeroAccessSupported && (_jsxs("div", { className: "mt-2 pt-4 border-t border-border", children: [_jsx("span", { className: "font-semibold text-sm block mb-1", children: "Zero-access encryption" }), _jsxs("label", { className: "flex items-start gap-2 text-sm", children: [_jsx("input", { type: "checkbox", className: "mt-1", checked: values.allowZeroAccess !== false, onChange: (e) => {
|
|
87
91
|
setSaved(false);
|
|
88
92
|
setValues({ ...values, allowZeroAccess: e.target.checked });
|
|
89
|
-
} }), _jsxs("span", { children: [_jsx("span", { className: "font-semibold block", children: "Allow zero-access encryption" }), _jsx("span", { className: "text-xs text-text-muted", children: "Mail that arrives unencrypted is encrypted the moment it is received, to a key only the mailbox\u2019s owner can unlock, and this server keeps no readable copy - administrators can\u2019t read it either, except through an escrow scope. People can still turn it off for their own mailbox. Search of message contents then happens in the app instead of on the server." })] })] })] })),
|
|
93
|
+
} }), _jsxs("span", { children: [_jsx("span", { className: "font-semibold block", children: "Allow zero-access encryption" }), _jsx("span", { className: "text-xs text-text-muted", children: "Mail that arrives unencrypted is encrypted the moment it is received, to a key only the mailbox\u2019s owner can unlock, and this server keeps no readable copy - administrators can\u2019t read it either, except through an escrow scope. People can still turn it off for their own mailbox. Search of message contents then happens in the app instead of on the server." })] })] })] })), openPgpSupported && (_jsxs("div", { className: "mt-2 pt-4 border-t border-border", children: [_jsx("span", { className: "font-semibold text-sm block mb-1", children: "OpenPGP" }), _jsxs("label", { className: "flex items-start gap-2 text-sm", children: [_jsx("input", { type: "checkbox", className: "mt-1", checked: values.allowOpenPgp !== false, onChange: (e) => {
|
|
94
|
+
setSaved(false);
|
|
95
|
+
setValues({ ...values, allowOpenPgp: e.target.checked });
|
|
96
|
+
} }), _jsxs("span", { children: [_jsx("span", { className: "font-semibold block", children: "Allow OpenPGP" }), _jsx("span", { className: "text-xs text-text-muted", children: "Gives every mailbox an OpenPGP key and publishes it, so people using Proton Mail, Thunderbird and other OpenPGP apps can send encrypted mail to your users, and your users to them. RapidMX still uses S/MIME whenever a recipient can receive it. Off, no OpenPGP keys are created or published." })] })] })] })), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] }), _jsxs(Modal, { open: confirmingProhibit !== null, onClose: () => setConfirmingProhibit(null), title: "Turn off encryption?", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Saving sets ", _jsx("strong", { children: confirmingProhibit?.join(", ") }), " to Never encrypt. Nobody on this server can then encrypt that mail end to end, whatever they have chosen for themselves, until an administrator allows it again."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingProhibit(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: confirmZeroAccessThenSave, children: "Save and turn off encryption" })] })] }), _jsxs(Modal, { open: confirmingZeroAccessOff, onClose: () => setConfirmingZeroAccessOff(false), title: "Turn off zero-access encryption?", children: [_jsx("p", { className: "text-sm mb-3", children: "Mail that arrives from now on will be stored in a form this server can read, for every mailbox. Mail that is already encrypted stays encrypted - this server can\u2019t decrypt it. Everyone sees in their settings that an administrator turned it off." }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingZeroAccessOff(false), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => void save(), children: "Save and turn off zero-access encryption" })] })] })] }));
|
|
90
97
|
}
|
|
@@ -8,10 +8,13 @@ import { HiOutlineBars3BottomLeft, HiOutlineBell, HiOutlineBriefcase, HiOutlineC
|
|
|
8
8
|
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
9
9
|
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
10
10
|
import { guestPermissionsOf, respondToEvent, visibilityOf } from "../../../../lib/calendar/calendarApi.js";
|
|
11
|
+
import { invitationContentOf } from "../../../../lib/calendar/protectedEvents.js";
|
|
12
|
+
import { organizerJoinLink } from "../../../../lib/videoconf/protectedMeetings.js";
|
|
11
13
|
import { htmlToPlainText } from "../../../../lib/calendar/eventDescription.js";
|
|
12
14
|
import { deleteEventOccurrence, deleteEventSeries } from "../../../../lib/calendar/calendarMutations.js";
|
|
13
15
|
import { describeRecurrence } from "../../../../lib/calendar/recurrence.js";
|
|
14
16
|
import { getVideoMeeting } from "../../../../lib/videoconf/videoMeetingsApi.js";
|
|
17
|
+
import { useKeysLocked } from "../../zeroAccess/useKeysLocked.js";
|
|
15
18
|
import { describeTimeZone, deviceTimeZone } from "../../../../lib/util/timeZone.js";
|
|
16
19
|
import Alert from "../../../../lib/components/feedback/Alert.js";
|
|
17
20
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
@@ -54,7 +57,7 @@ export default function EventDetails({ occurrence, isInvited, myResponse, isOwnA
|
|
|
54
57
|
}
|
|
55
58
|
let cancelled = false;
|
|
56
59
|
void getVideoMeeting(videoMeetingUid, client)
|
|
57
|
-
.then(
|
|
60
|
+
.then(organizerJoinLink)
|
|
58
61
|
.catch(() => null)
|
|
59
62
|
.then((url) => {
|
|
60
63
|
if (!cancelled) {
|
|
@@ -65,6 +68,13 @@ export default function EventDetails({ occurrence, isInvited, myResponse, isOwnA
|
|
|
65
68
|
cancelled = true;
|
|
66
69
|
};
|
|
67
70
|
}, [videoMeetingUid, client]);
|
|
71
|
+
// A protected meeting's link carries its key after `#`, opened with the mailbox's keys: it goes when they lock, whether or not the
|
|
72
|
+
// event itself is protected (a meeting is keyed whenever the mailbox has zero-access, its calendar protected or not).
|
|
73
|
+
useKeysLocked((locked) => {
|
|
74
|
+
if (locked === occurrence.mailboxUid) {
|
|
75
|
+
setOrganizerJoinUrl((url) => (url?.includes("#") ? null : url));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
68
78
|
// The same scheme allow-list a reminder's own location gets: a hostile or malformed join link never reaches `window.open()`.
|
|
69
79
|
const joinUrl = joinMeetingUrl(organizerJoinUrl);
|
|
70
80
|
async function handleDelete(scope) {
|
|
@@ -91,7 +101,7 @@ export default function EventDetails({ occurrence, isInvited, myResponse, isOwnA
|
|
|
91
101
|
setError(null);
|
|
92
102
|
setBusy(true);
|
|
93
103
|
try {
|
|
94
|
-
await respondToEvent(occurrence.uid, responseStatus, client);
|
|
104
|
+
await respondToEvent(occurrence.uid, responseStatus, client, occurrence);
|
|
95
105
|
// A decline soft-deletes the mailbox's own copy server-side - treat it the same as a
|
|
96
106
|
// delete rather than a save so the calendar view drops it immediately.
|
|
97
107
|
if (responseStatus === "declined") {
|
|
@@ -137,6 +147,7 @@ export default function EventDetails({ occurrence, isInvited, myResponse, isOwnA
|
|
|
137
147
|
description: occurrence.description,
|
|
138
148
|
descriptionHtml: occurrence.descriptionHtml,
|
|
139
149
|
guestAddresses: occurrence.attendees.map((a) => a.address),
|
|
150
|
+
invitationContent: invitationContentOf(occurrence),
|
|
140
151
|
}, canChange: canChange, canInvite: canInvite, onSent: () => setRequesting(false), onCancel: () => setRequesting(false) }) })), myResponse !== undefined && canWrite && (_jsxs("div", { className: "flex flex-col gap-2 mx-5 mb-3 p-3 rounded-md bg-surface-alt", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 flex-wrap", children: [_jsx("span", { className: "text-sm font-medium", children: "Your response" }), _jsx("div", { className: "flex gap-2", children: answers.map(({ response, label }) => {
|
|
141
152
|
const current = myResponse === response;
|
|
142
153
|
return (_jsxs(Button, { type: "button", variant: "secondary", className: ["!w-auto", response === "declined" ? "text-danger" : "", current ? "!bg-primary/10 !border-primary" : ""].join(" "), "aria-pressed": current, disabled: busy, onClick: () => (occurrence.isRecurringOccurrence ? setPendingResponse(response) : void handleRespond(response)), children: [current && _jsx(HiOutlineCheck, { size: 14, "aria-hidden": "true" }), label] }, response));
|