@rapidmx/web-client 0.35.0 → 0.36.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/sealedEvents.ts +80 -0
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +61 -3
- package/apps/shared/components/contacts/contactCardData.ts +2 -1
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +23 -1
- package/apps/shared/components/layout/SharedMailboxKeyManager.tsx +91 -0
- package/apps/shared/components/layout/UnlockPromptProvider.tsx +34 -0
- package/apps/shared/components/mail/ConversationList.tsx +1 -1
- package/apps/shared/components/mail/MessageDetailPane.tsx +35 -13
- package/apps/shared/components/mail/compose/draftResume.ts +28 -3
- package/apps/shared/components/mail/compose/quotedBody.ts +4 -3
- package/apps/shared/components/mail/invite/inviteStore.ts +7 -1
- package/apps/shared/components/mail/layout/MailShell.tsx +2 -0
- package/apps/shared/components/mail/reading/useMessageActions.ts +4 -1
- package/apps/shared/components/settings/KeyExportSetting.tsx +170 -0
- package/apps/shared/components/settings/PolicyLockedNotice.tsx +23 -0
- package/apps/shared/components/settings/ZeroAccessSetting.tsx +117 -0
- package/apps/shared/mail/decryptedMessages.ts +11 -3
- package/apps/shared/mail/outbox/sendJob.ts +2 -2
- package/apps/shared/mail/reportNotices.ts +3 -1
- package/apps/shared/mail/zeroAccessSource.ts +27 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +31 -0
- package/apps/shared/search/localIndexBuilder.ts +27 -1
- package/apps/shared/search/localIndexRpcClient.ts +8 -4
- package/apps/shared/search/localIndexSchema.ts +2 -2
- package/apps/shared/search/localIndexTransport.ts +2 -1
- package/apps/shared/search/localIndexWorker.ts +6 -4
- package/apps/shared/search/zeroAccessIndexSync.ts +487 -0
- package/apps/www/calendar/index.tsx +176 -56
- package/apps/www/index.tsx +10 -3
- package/apps/www/settings/encryption/index.tsx +5 -0
- package/apps/www/settings/privacy/index.tsx +2 -1
- package/dist/apps/escrow/matters/[uid].js +2 -2
- package/dist/apps/shared/calendar/sealedEvents.d.ts +13 -0
- package/dist/apps/shared/calendar/sealedEvents.js +54 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +18 -2
- package/dist/apps/shared/components/contacts/contactCardData.js +2 -1
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +21 -2
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.d.ts +15 -0
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.js +72 -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/MessageDetailPane.js +34 -13
- 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/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 +6 -1
- package/dist/apps/shared/components/mail/layout/MailShell.js +2 -1
- 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 +88 -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 +14 -0
- package/dist/apps/shared/components/settings/ZeroAccessSetting.js +71 -0
- 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.js +2 -2
- 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 +28 -0
- package/dist/apps/shared/search/localIndexBuilder.d.ts +5 -0
- package/dist/apps/shared/search/localIndexBuilder.js +24 -2
- package/dist/apps/shared/search/localIndexRpcClient.d.ts +1 -1
- package/dist/apps/shared/search/localIndexRpcClient.js +6 -4
- package/dist/apps/shared/search/localIndexSchema.d.ts +1 -1
- package/dist/apps/shared/search/localIndexSchema.js +2 -2
- package/dist/apps/shared/search/localIndexTransport.d.ts +2 -1
- package/dist/apps/shared/search/localIndexWorker.d.ts +2 -0
- package/dist/apps/shared/search/localIndexWorker.js +4 -4
- package/dist/apps/shared/search/zeroAccessIndexSync.d.ts +71 -0
- package/dist/apps/shared/search/zeroAccessIndexSync.js +382 -0
- package/dist/apps/www/calendar/index.js +41 -15
- package/dist/apps/www/index.js +10 -3
- package/dist/apps/www/settings/encryption/index.js +3 -1
- package/dist/apps/www/settings/privacy/index.js +1 -1
- package/dist/lib/admin/escrowAccessRequestsApi.d.ts +6 -0
- package/dist/lib/calendar/calendarApi.d.ts +6 -0
- package/dist/lib/calendar/inviteApi.js +27 -5
- package/dist/lib/calendar/sealedInvites.d.ts +11 -0
- package/dist/lib/calendar/sealedInvites.js +28 -0
- package/dist/lib/crypto/keyExport.d.ts +54 -0
- package/dist/lib/crypto/keyExport.js +161 -0
- package/dist/lib/crypto/keySession.d.ts +45 -1
- package/dist/lib/crypto/keySession.js +93 -2
- package/dist/lib/crypto/keyvaultApi.d.ts +88 -3
- package/dist/lib/crypto/keyvaultApi.js +32 -0
- package/dist/lib/crypto/messageSecurity.d.ts +30 -1
- package/dist/lib/crypto/messageSecurity.js +80 -4
- package/dist/lib/crypto/mime.d.ts +5 -0
- package/dist/lib/crypto/mime.js +25 -0
- package/dist/lib/crypto/sharedMailboxKeys.d.ts +50 -0
- package/dist/lib/crypto/sharedMailboxKeys.js +264 -0
- package/dist/lib/crypto/zeroAccessKey.d.ts +32 -0
- package/dist/lib/crypto/zeroAccessKey.js +66 -0
- package/dist/lib/mail/conversationsApi.d.ts +6 -0
- package/dist/lib/mail/mailApi.d.ts +30 -2
- package/dist/lib/mail/mailApi.js +5 -1
- package/dist/lib/search/zeroAccessIndexApi.d.ts +81 -0
- package/dist/lib/search/zeroAccessIndexApi.js +35 -0
- package/lib/admin/escrowAccessRequestsApi.ts +6 -0
- package/lib/calendar/calendarApi.ts +3 -0
- package/lib/calendar/inviteApi.ts +28 -5
- package/lib/calendar/sealedInvites.ts +33 -0
- package/lib/crypto/keyExport.ts +229 -0
- package/lib/crypto/keySession.ts +117 -3
- package/lib/crypto/keyvaultApi.ts +125 -3
- package/lib/crypto/messageSecurity.ts +110 -3
- package/lib/crypto/mime.ts +26 -0
- package/lib/crypto/sharedMailboxKeys.ts +324 -0
- package/lib/crypto/zeroAccessKey.ts +97 -0
- package/lib/mail/conversationsApi.ts +3 -0
- package/lib/mail/mailApi.ts +33 -3
- package/lib/search/zeroAccessIndexApi.ts +119 -0
- package/package.json +3 -3
|
@@ -13,15 +13,16 @@ import { ApiRequestError } from "../../../lib/util/api.js";
|
|
|
13
13
|
import { useApiClient } from "../../../lib/util/apiClientContext.js";
|
|
14
14
|
import useIsMobile from "../../../lib/util/useIsMobile.js";
|
|
15
15
|
import { getCalendarEvent, listCalendarEvents } from "../../../lib/calendar/calendarApi.js";
|
|
16
|
+
import { openSealedEvent, openSealedEvents } from "../../shared/calendar/sealedEvents.js";
|
|
16
17
|
import { useFolderLiveUpdates } from "../../shared/live/useFolderLiveUpdates.js";
|
|
17
18
|
import { moveOccurrence, resizeOccurrenceEnd } from "../../../lib/calendar/calendarMutations.js";
|
|
18
19
|
import { resolveDragAction } from "../../../lib/calendar/calendarDragIds.js";
|
|
19
20
|
import { createFolder } from "../../../lib/mail/mailApi.js";
|
|
20
21
|
import { expandAllOccurrences, expandOccurrences } from "../../../lib/calendar/recurrence.js";
|
|
21
22
|
import { useDayKey } from "../../../lib/calendar/useDayKey.js";
|
|
22
|
-
import { allOccurrences, initialMatchIndex, occurrencesInRange, searchOccurrences, searchTerms, stepSequenceIndex } from "../../../lib/calendar/eventSearch.js";
|
|
23
|
+
import { allOccurrences, initialMatchIndex, occurrencesInRange, searchOccurrences, searchTerms, stepSequenceIndex, } from "../../../lib/calendar/eventSearch.js";
|
|
23
24
|
import Drawer from "../../../lib/components/overlays/Drawer.js";
|
|
24
|
-
import CalendarShell, { useCalendarShell } from "../../shared/components/calendar/layout/CalendarShell.js";
|
|
25
|
+
import CalendarShell, { useCalendarShell, } from "../../shared/components/calendar/layout/CalendarShell.js";
|
|
25
26
|
import CalendarListSidebar from "../../shared/components/calendar/CalendarListSidebar.js";
|
|
26
27
|
import CalendarSearchBox from "../../shared/components/calendar/CalendarSearchBox.js";
|
|
27
28
|
import { ActiveOccurrenceContext } from "../../shared/components/calendar/activeOccurrence.js";
|
|
@@ -37,7 +38,7 @@ import SplitDayView from "../../shared/components/calendar/SplitDayView.js";
|
|
|
37
38
|
import TimeGridView from "../../shared/components/calendar/TimeGridView.js";
|
|
38
39
|
import Alert from "../../../lib/components/feedback/Alert.js";
|
|
39
40
|
import Button from "../../../lib/components/buttons/Button.js";
|
|
40
|
-
import { CALENDAR_VIEWS, getStoredCalendarView, isCalendarView, storeCalendarView } from "../../shared/components/calendar/calendarView.js";
|
|
41
|
+
import { CALENDAR_VIEWS, getStoredCalendarView, isCalendarView, storeCalendarView, } from "../../shared/components/calendar/calendarView.js";
|
|
41
42
|
import { SWIPE_PERIOD_SHIFT } from "../../shared/components/calendar/swipeNavigation.js";
|
|
42
43
|
import { nextHalfHour } from "../../shared/components/calendar/timePicker.js";
|
|
43
44
|
import { useSwipeSlide } from "../../shared/gestures/useSwipeSlide.js";
|
|
@@ -56,7 +57,14 @@ const MAX_DATE_MS = 8.64e15;
|
|
|
56
57
|
function CalendarPage(props) {
|
|
57
58
|
return (_jsx(CalendarShell, { ...props, children: _jsx(CalendarContent, { userUid: props.userUid, bookingHref: bookingSettingsHref(props.pluginNav) }) }));
|
|
58
59
|
}
|
|
59
|
-
const VIEW_LABELS = {
|
|
60
|
+
const VIEW_LABELS = {
|
|
61
|
+
list: "List",
|
|
62
|
+
month: "Month",
|
|
63
|
+
week: "Week",
|
|
64
|
+
workWeek: "Work Week",
|
|
65
|
+
day: "Day",
|
|
66
|
+
split: "Split",
|
|
67
|
+
};
|
|
60
68
|
/** The shortcut that switches to a view (Outlook's Ctrl+Alt+1-4, the List one more); the split view has none. */
|
|
61
69
|
const VIEW_SHORTCUTS = {
|
|
62
70
|
list: SHORTCUTS.calendar.list,
|
|
@@ -66,7 +74,7 @@ const VIEW_SHORTCUTS = {
|
|
|
66
74
|
month: SHORTCUTS.calendar.month,
|
|
67
75
|
};
|
|
68
76
|
/** One of the view switcher's buttons, with its shortcut in the tooltip. */
|
|
69
|
-
function ViewButton({ view, current, onSelect }) {
|
|
77
|
+
function ViewButton({ view, current, onSelect, }) {
|
|
70
78
|
const shortcut = VIEW_SHORTCUTS[view];
|
|
71
79
|
const hint = useShortcutProps(VIEW_LABELS[view], shortcut ?? SHORTCUTS.calendar.day, !!shortcut);
|
|
72
80
|
return (_jsx("button", { type: "button", onClick: () => onSelect(view), ...hint, className: [
|
|
@@ -198,7 +206,7 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
198
206
|
}
|
|
199
207
|
setLoading(true);
|
|
200
208
|
setError(null);
|
|
201
|
-
void Promise.allSettled(targets.map((f) => listCalendarEvents(f.uid, client)))
|
|
209
|
+
void Promise.allSettled(targets.map((f) => listCalendarEvents(f.uid, client).then(openSealedEvents)))
|
|
202
210
|
.then((results) => {
|
|
203
211
|
if (seq !== reloadSeqRef.current) {
|
|
204
212
|
return;
|
|
@@ -237,7 +245,10 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
237
245
|
}
|
|
238
246
|
// Keyed on stable string summaries (not the array/set objects themselves, which are new references
|
|
239
247
|
// every render) so this only re-fires when the actual set of folders to fetch changes.
|
|
240
|
-
const calendarFolderUidsKey = calendarFolders
|
|
248
|
+
const calendarFolderUidsKey = calendarFolders
|
|
249
|
+
.map((f) => f.uid)
|
|
250
|
+
.sort()
|
|
251
|
+
.join(",");
|
|
241
252
|
const checkedFolderUidsKey = Array.from(checkedFolderUids).sort().join(",");
|
|
242
253
|
useEffect(reload, [calendarFolderUidsKey, checkedFolderUidsKey]);
|
|
243
254
|
// An event that somebody creates, changes or deletes is announced on the push connection - a private or confidential one as its busy block, whatever the
|
|
@@ -254,7 +265,9 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
254
265
|
setEvents((previous) => previous.filter((existing) => existing.uid !== uid));
|
|
255
266
|
return;
|
|
256
267
|
}
|
|
257
|
-
void getCalendarEvent(uid, client)
|
|
268
|
+
void getCalendarEvent(uid, client)
|
|
269
|
+
.then(openSealedEvent)
|
|
270
|
+
.then((fresh) => {
|
|
258
271
|
if (checkedFolderUidsRef.current.has(fresh.folderUid)) {
|
|
259
272
|
setEvents((previous) => previous.some((existing) => existing.uid === fresh.uid)
|
|
260
273
|
? previous.map((existing) => (existing.uid === fresh.uid ? fresh : existing))
|
|
@@ -305,7 +318,9 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
305
318
|
if (view === "list") {
|
|
306
319
|
return sequence;
|
|
307
320
|
}
|
|
308
|
-
return searching
|
|
321
|
+
return searching
|
|
322
|
+
? occurrencesInRange(matches, rangeStart, rangeEnd)
|
|
323
|
+
: expandAllOccurrences(events, rangeStart, rangeEnd);
|
|
309
324
|
}, [events, matches, searching, view, sequence, rangeStart, rangeEnd]);
|
|
310
325
|
/** Points at `match`, moving the view to the day it is on unless the view already shows it. */
|
|
311
326
|
function showMatch(match) {
|
|
@@ -354,7 +369,9 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
354
369
|
searchedQueryRef.current = query;
|
|
355
370
|
// The List holds every match: it starts at the first from the day being viewed on rather than the first of all.
|
|
356
371
|
const from = startOfDay(viewDate);
|
|
357
|
-
showMatch(matches[view === "list"
|
|
372
|
+
showMatch(matches[view === "list"
|
|
373
|
+
? initialMatchIndex(matches, from, rangeEnd, from)
|
|
374
|
+
: initialMatchIndex(matches, rangeStart, rangeEnd, from)]);
|
|
358
375
|
}
|
|
359
376
|
}, [matches, query, searching]);
|
|
360
377
|
const splitColumns = useMemo(() => calendarFolders
|
|
@@ -407,7 +424,9 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
407
424
|
// Brings the match being pointed at into sight (the views scroll their own hours; the page may too).
|
|
408
425
|
useEffect(() => {
|
|
409
426
|
const marked = Array.from(swipe.ref.current.querySelectorAll("[data-occurrence-key]"));
|
|
410
|
-
marked
|
|
427
|
+
marked
|
|
428
|
+
.find((el) => el.dataset.occurrenceKey === activeKey)
|
|
429
|
+
?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
411
430
|
}, [activeKey, view === "list" ? null : viewDate, view, loading]);
|
|
412
431
|
// The List scrolls to `viewDate` when this changes (Today, a day picked in the mini calendar), even when it is the date it already has.
|
|
413
432
|
const [jumpNonce, setJumpNonce] = useState(0);
|
|
@@ -461,10 +480,12 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
461
480
|
const requestedStart = new Date(params.get("start") ?? "");
|
|
462
481
|
navigateTo("/calendar", { replace: true });
|
|
463
482
|
getCalendarEvent(eventUid, client)
|
|
483
|
+
.then(openSealedEvent)
|
|
464
484
|
.then((event) => {
|
|
465
485
|
const from = isNaN(requestedStart.getTime()) ? new Date(event.startDate) : requestedStart;
|
|
466
486
|
const candidates = expandOccurrences(event, new Date(from.getTime() - 86400000), new Date(from.getTime() + 86400000));
|
|
467
|
-
const match = candidates.find((occurrence) => new Date(occurrence.startDate).getTime() === from.getTime()) ??
|
|
487
|
+
const match = candidates.find((occurrence) => new Date(occurrence.startDate).getTime() === from.getTime()) ??
|
|
488
|
+
candidates[0];
|
|
468
489
|
if (match) {
|
|
469
490
|
setViewDate(occurrenceDay(match));
|
|
470
491
|
openEvent(match);
|
|
@@ -535,12 +556,17 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
535
556
|
const writableMailboxes = useWritableMailboxes(mailboxes, userUid, modalMailboxUid);
|
|
536
557
|
const mailboxOptions = useMemo(() => mailboxCalendars
|
|
537
558
|
.filter((mc) => mc.calendarFolders.length > 0 && writableMailboxes.some((mb) => mb.uid === mc.mailbox.uid))
|
|
538
|
-
.map((mc) => ({
|
|
559
|
+
.map((mc) => ({
|
|
560
|
+
mailbox: mc.mailbox,
|
|
561
|
+
calendars: mc.calendarFolders.map((f) => ({ uid: f.uid, name: f.name })),
|
|
562
|
+
})), [mailboxCalendars, writableMailboxes]);
|
|
539
563
|
const sidebarContent = (_jsxs(_Fragment, { children: [_jsx(MiniDatePicker, { selected: viewDate, onSelect: (date) => {
|
|
540
564
|
setViewDate(startOfDay(date));
|
|
541
565
|
setJumpNonce((n) => n + 1);
|
|
542
566
|
} }), _jsx(CalendarListSidebar, { mailboxCalendars: mailboxCalendars, checkedFolderUids: checkedFolderUids, onToggle: toggleCalendar, onAddCalendar: handleAddCalendar, colorFor: colorFor })] }));
|
|
543
|
-
return (_jsxs("div", { className: "flex-1 min-w-0 flex min-h-0", children: [_jsx("div", { className: "hidden lg:flex w-56 shrink-0 border-r border-border flex-col overflow-y-auto", children: sidebarContent }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Calendars", fullScreen: true, children: _jsx("div", { className: "flex flex-col", children: sidebarContent }) }), _jsxs("div", { className: "flex-1 min-w-0 flex flex-col min-h-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2 p-3 border-b border-border shrink-0", children: [_jsx("button", { type: "button", className: "lg:hidden w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text shrink-0", "aria-label": "Open calendars", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), !isMobile && (_jsx(Button, { type: "button", onClick: (e) => openNewEvent(undefined, undefined, undefined, anchorOf(e.currentTarget, "below")), className: "!w-auto shrink-0", ...newEventHint, children: "+ New event" })), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => shiftView(-1), "aria-label": "Previous", ...previousHint, className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u2039" }), _jsx("button", { type: "button", onClick: goToday, ...todayHint, className: "px-2 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "Today" }), _jsx("button", { type: "button", onClick: () => shiftView(1), "aria-label": "Next", ...nextHint, className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u203A" })] }), _jsx("h1", { className: "hidden md:block text-lg font-bold tracking-tight", children: title }), _jsx(CalendarSearchBox, { value: searchText, onChange: setSearchText, onSubmit: submitSearch, onClear: clearSearch, onPrevious: () => stepMatch(-1), onNext: () => stepMatch(1), active: searching, count: matches.length, position: activeIndex, inputRef: searchInputRef }), _jsx("div", { className: "flex flex-wrap gap-1", children: CALENDAR_VIEWS.map((v) => (_jsx(ViewButton, { view: v, current: view, onSelect: setView }, v))) })] }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), _jsx("div", { ref: swipe.ref, className: "flex-1 min-w-0 flex flex-col min-h-0 overflow-hidden", ...swipe.handlers, style: swipe.style, children: _jsx("div", { "data-swipe-phase": swipe.phase, className: "flex-1 min-w-0 flex flex-col min-h-0", style: swipe.contentStyle, children: _jsx("div", { className: "flex-1 min-w-0 flex flex-col min-h-0", style: wheelSlide.style, children: loading ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : (_jsx(ActiveOccurrenceContext.Provider, { value: searching ? activeKey : null, children: _jsx(DndContext, { sensors: sensors, onDragStart: () => setDragging(true), onDragEnd: handleDragEnd, onDragCancel: () => setDragging(false), children: view === "list" ? (_jsx(ListView, { occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, focusDate: viewDate, jumpNonce: jumpNonce, step: listStep })) : view === "month" ? (_jsx(MonthView, { viewDate: viewDate, occurrences: occurrences, folderColors: folderColors, onSelectDay: handleSelectDay, onSelectEvent: openEvent, onSelectSlot: (start, end, anchor) => openNewEvent(start, end, undefined, anchor, true) })) : view === "split" ? (_jsx(SplitDayView, { day: viewDate, columns: splitColumns, occurrences: occurrences, onSelectEvent: openEvent, onSelectSlot: (start, end, targetFolderUid, anchor) => openNewEvent(start, end, targetFolderUid, anchor) })) : (_jsx(TimeGridView, { days: days, occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, onSelectSlot: (start, end, anchor) => openNewEvent(start, end, undefined, anchor) })) }) })) }) }) }), modal && modalMailbox && (_jsx(EventModal, { open: true, onClose: closeModal, mailboxUid: modalMailbox.uid, folderUid: modalFolderUid, calendars: calendarFolders
|
|
567
|
+
return (_jsxs("div", { className: "flex-1 min-w-0 flex min-h-0", children: [_jsx("div", { className: "hidden lg:flex w-56 shrink-0 border-r border-border flex-col overflow-y-auto", children: sidebarContent }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Calendars", fullScreen: true, children: _jsx("div", { className: "flex flex-col", children: sidebarContent }) }), _jsxs("div", { className: "flex-1 min-w-0 flex flex-col min-h-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2 p-3 border-b border-border shrink-0", children: [_jsx("button", { type: "button", className: "lg:hidden w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text shrink-0", "aria-label": "Open calendars", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), !isMobile && (_jsx(Button, { type: "button", onClick: (e) => openNewEvent(undefined, undefined, undefined, anchorOf(e.currentTarget, "below")), className: "!w-auto shrink-0", ...newEventHint, children: "+ New event" })), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => shiftView(-1), "aria-label": "Previous", ...previousHint, className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u2039" }), _jsx("button", { type: "button", onClick: goToday, ...todayHint, className: "px-2 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "Today" }), _jsx("button", { type: "button", onClick: () => shiftView(1), "aria-label": "Next", ...nextHint, className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u203A" })] }), _jsx("h1", { className: "hidden md:block text-lg font-bold tracking-tight", children: title }), _jsx(CalendarSearchBox, { value: searchText, onChange: setSearchText, onSubmit: submitSearch, onClear: clearSearch, onPrevious: () => stepMatch(-1), onNext: () => stepMatch(1), active: searching, count: matches.length, position: activeIndex, inputRef: searchInputRef }), _jsx("div", { className: "flex flex-wrap gap-1", children: CALENDAR_VIEWS.map((v) => (_jsx(ViewButton, { view: v, current: view, onSelect: setView }, v))) })] }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), _jsx("div", { ref: swipe.ref, className: "flex-1 min-w-0 flex flex-col min-h-0 overflow-hidden", ...swipe.handlers, style: swipe.style, children: _jsx("div", { "data-swipe-phase": swipe.phase, className: "flex-1 min-w-0 flex flex-col min-h-0", style: swipe.contentStyle, children: _jsx("div", { className: "flex-1 min-w-0 flex flex-col min-h-0", style: wheelSlide.style, children: loading ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : (_jsx(ActiveOccurrenceContext.Provider, { value: searching ? activeKey : null, children: _jsx(DndContext, { sensors: sensors, onDragStart: () => setDragging(true), onDragEnd: handleDragEnd, onDragCancel: () => setDragging(false), children: view === "list" ? (_jsx(ListView, { occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, focusDate: viewDate, jumpNonce: jumpNonce, step: listStep })) : view === "month" ? (_jsx(MonthView, { viewDate: viewDate, occurrences: occurrences, folderColors: folderColors, onSelectDay: handleSelectDay, onSelectEvent: openEvent, onSelectSlot: (start, end, anchor) => openNewEvent(start, end, undefined, anchor, true) })) : view === "split" ? (_jsx(SplitDayView, { day: viewDate, columns: splitColumns, occurrences: occurrences, onSelectEvent: openEvent, onSelectSlot: (start, end, targetFolderUid, anchor) => openNewEvent(start, end, targetFolderUid, anchor) })) : (_jsx(TimeGridView, { days: days, occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, onSelectSlot: (start, end, anchor) => openNewEvent(start, end, undefined, anchor) })) }) })) }) }) }), modal && modalMailbox && (_jsx(EventModal, { open: true, onClose: closeModal, mailboxUid: modalMailbox.uid, folderUid: modalFolderUid, calendars: calendarFolders
|
|
568
|
+
.filter((f) => f.mailboxUid === modalMailbox.uid)
|
|
569
|
+
.map((f) => ({ uid: f.uid, name: f.name })), mailboxOptions: mailboxOptions, folderColors: folderColors, organizerAddress: modalMailbox.primarySmtpAddress, organizerAliases: modalMailbox.aliasAddresses, occurrence: modal.occurrence, initialStart: modal.initialStart, initialEnd: modal.initialEnd, initialAllDay: modal.initialAllDay, anchor: modal.anchor, bookingHref: bookingHref, matchNav: modal.occurrence && sequence.length > 0
|
|
544
570
|
? {
|
|
545
571
|
onPrevious: () => stepMatch(-1),
|
|
546
572
|
onNext: () => stepMatch(1),
|
|
@@ -549,7 +575,7 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
549
575
|
noun: searching ? "match" : "event",
|
|
550
576
|
slide,
|
|
551
577
|
}
|
|
552
|
-
: undefined, onSaved: handleSaved, onDeleted: handleDeleted })), isMobile && mailboxUid && folderUid && !modal && _jsx(FloatingActionButton, { label: "New event", icon: HiOutlinePlus, onClick: () => openNewEvent() })] })] }));
|
|
578
|
+
: undefined, onSaved: handleSaved, onDeleted: handleDeleted })), isMobile && mailboxUid && folderUid && !modal && (_jsx(FloatingActionButton, { label: "New event", icon: HiOutlinePlus, onClick: () => openNewEvent() }))] })] }));
|
|
553
579
|
}
|
|
554
580
|
export default CalendarPage;
|
|
555
581
|
/** The tab's title: `Brand: Calendar` (see `pageTitle()`). */
|
package/dist/apps/www/index.js
CHANGED
|
@@ -100,11 +100,18 @@ async function decryptEncryptedRows(messages, unlocked) {
|
|
|
100
100
|
// A few at a time: each is a download and a public-key decryption.
|
|
101
101
|
const entries = await mapWithConcurrency(encrypted, DECRYPT_CONCURRENCY, async (message) => {
|
|
102
102
|
try {
|
|
103
|
-
const rawMime = await getMessageRawContent(message.uid);
|
|
104
103
|
// Loaded here, on first use: the S/MIME code (PKI.js, ASN.1, X.509) is over half a megabyte and an inbox with
|
|
105
104
|
// no encrypted mail in it never needs it.
|
|
106
|
-
const
|
|
107
|
-
|
|
105
|
+
const securityApi = await (securityModule ?? (securityModule = import("../../lib/crypto/messageSecurity.js")));
|
|
106
|
+
// A message sealed at rest carries its subject and preview sealed on the row: no download needed.
|
|
107
|
+
const summary = message.zeroAccess ? await securityApi.openZeroAccessSummary(message.zeroAccess.sealedSummary, unlocked.zeroAccessKeys) : undefined;
|
|
108
|
+
if (summary) {
|
|
109
|
+
const preview = summary.preview.slice(0, LIST_PREVIEW_MAX_LENGTH);
|
|
110
|
+
rememberDecrypted(message.uid, message.mailboxUid, { subject: summary.subject, preview });
|
|
111
|
+
return [message.uid, { subject: summary.subject, preview }];
|
|
112
|
+
}
|
|
113
|
+
const rawMime = await getMessageRawContent(message.uid);
|
|
114
|
+
const security = await securityApi.evaluateMessageSecurity(rawMime, unlocked);
|
|
108
115
|
if (!security.subject && !security.html) {
|
|
109
116
|
markDecryptFailed(message.uid);
|
|
110
117
|
return null;
|
|
@@ -25,6 +25,8 @@ import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
|
|
|
25
25
|
import Alert from "../../../../lib/components/feedback/Alert.js";
|
|
26
26
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
27
27
|
import Modal from "../../../../lib/components/overlays/Modal.js";
|
|
28
|
+
import ZeroAccessSetting from "../../../shared/components/settings/ZeroAccessSetting.js";
|
|
29
|
+
import KeyExportSetting from "../../../shared/components/settings/KeyExportSetting.js";
|
|
28
30
|
import SigningCertificateCard from "../../../shared/components/settings/SigningCertificateCard.js";
|
|
29
31
|
import { readStoredSignEnrollment, storeSignEnrollment } from "../../../shared/signing/enrollmentStorage.js";
|
|
30
32
|
import { checkEnrollmentNow, forgetEnrollment, recordEnrollmentResult, useEnrollmentSnapshot, watchCurrentEnrollment, watchEnrollment, } from "../../../shared/signing/enrollmentTracker.js";
|
|
@@ -799,7 +801,7 @@ function EncryptionContent({ canManageKeys }) {
|
|
|
799
801
|
: null;
|
|
800
802
|
const certificateEnrollment = certificateMode === "issued" && enrollmentResult?.status !== "issued" ? null : certificateMode === "expired" ? null : enrollmentResult;
|
|
801
803
|
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl flex flex-col gap-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Encryption" }), _jsxs("p", { className: "text-sm text-text-muted", children: ["Manage how ", mailbox.displayName, " unlocks its encryption keys on this and other devices."] })] }), loadError && _jsx(Alert, { children: loadError }), actionError && _jsx(Alert, { children: actionError }), !canManageKeys && (_jsx("p", { className: "text-sm text-text-muted", children: "Only this mailbox\u2019s owner, signed in as themselves, can change its encryption keys and unlock methods." })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Encryption keys" }), displayedKeys.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-1 text-sm", children: displayedKeys.map((key) => (_jsxs("li", { className: "font-mono text-xs", children: [key.useType === "sign" ? "Signing" : "Encryption", " key: ", key.fingerprint, key.revokedAt &&
|
|
802
|
-
(key.revocationReason === "superseded" ? (_jsx("span", { className: "text-text-muted", children: " (superseded)" })) : (_jsx("span", { className: "text-danger", children: " (revoked)" })))] }, key.fingerprint))) })) : (_jsx("p", { className: "text-sm text-text-muted", children: "No keys enrolled yet." }))] }), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Digital signatures" }), activeSigningKey && signingStatus !== "pending" && (_jsx("p", { className: ["text-sm text-text-muted", certificateMode ? "mb-3" : ""].join(" "), children: "Enabled \u2014 outgoing mail from this mailbox is signed with a publicly-trusted certificate." })), certificateMode && (_jsxs("div", { className: "flex flex-col gap-2", children: [certificateMode === "failed" && signingError && _jsx(Alert, { children: signingError }), _jsx(SigningCertificateCard, { mode: certificateMode, enrollment: certificateEnrollment, snapshot: enrollment, info: signingInfo, address: mailbox.primarySmtpAddress, keyNotAfter: certificateMode === "expired" ? expiredSigningKey?.notAfter : activeSigningKey?.notAfter, canRequest: canManageKeys, requesting: signingStatus === "enrolling", onCheck: canManageKeys ? () => void checkEnrollmentNow(mailboxUid) : undefined, onRequest: handleEnrollSigning })] })), !activeSigningKey && !certificateMode && !canManageKeys ? (_jsx("p", { className: "text-sm text-text-muted", children: "Not enabled." })) : activeSigningKey || certificateMode ? null : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-xs text-text-muted", children: "Lets recipients verify that mail from this mailbox is genuinely from you. Optional \u2014 encryption already works without it." }), beforeRequestText(signingInfo, mailbox.primarySmtpAddress) && (_jsx("p", { className: "text-xs text-text-muted", children: beforeRequestText(signingInfo, mailbox.primarySmtpAddress) })), healthWarning(signingInfo) && _jsx(Alert, { children: healthWarning(signingInfo) }), enrollment?.gone && _jsx(Alert, { children: STALE_REQUEST_TEXT }), signingError && _jsx(Alert, { children: signingError }), signingInfo?.backend !== "none" && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: signingStatus === "enrolling", disabled: signingStatus !== "idle", onClick: handleEnrollSigning, children: "Enable digital signatures" }))] }))] }), mailbox.escrowScopeId && (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Escrow" }), escrowError && _jsx(Alert, { children: escrowError }), hasEscrowWrap ? (_jsx("p", { className: "text-sm text-text-muted", children: "This mailbox is under legal/compliance escrow \u2014 an authorized holder in your organization can recover its encrypted mail if needed. This does not weaken protection against anyone else." })) : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("p", { className: "text-xs text-text-muted", children: ["Your organization has assigned this mailbox to an escrow scope, but nothing has been protected yet \u2014 an authorized holder cannot recover this mailbox\u2019s encrypted mail until you complete this step.", " ", "This does not weaken protection against anyone else."] }), canManageKeys && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: wrappingEscrow, disabled: wrappingEscrow, onClick: handleWrapEscrow, children: "Add escrow protection" }))] })), _jsxs(Modal, { open: pendingEscrow !== null, onClose: () => setPendingEscrow(null), title: "Confirm escrow certificate", children: [_jsx("p", { className: "text-sm mb-3", children: "Anyone holding the private key for this certificate will be able to recover this mailbox\u2019s encrypted mail. Confirm it is the escrow certificate your organization expects before continuing." }), _jsxs("dl", { className: "text-sm mb-5 flex flex-col gap-1", children: [_jsxs("div", { children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Escrow scope" }), _jsx("dd", { className: "break-all", children: pendingEscrow?.escrowScopeId })] }), _jsxs("div", { children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Certificate fingerprint" }), _jsx("dd", { className: "font-mono break-all", children: pendingEscrow?.publicKey.fingerprint })] })] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setPendingEscrow(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => void handleConfirmEscrow(pendingEscrow), children: "Trust this certificate" })] })] })] })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Unlock methods" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["Removing a method here stops it from being usable to unlock this mailbox going forward, but it is ", _jsx("strong", { children: "not" }), " full revocation \u2014 anyone who already captured a wrapped copy and knows its secret could still use it. For real revocation (e.g. after a lost device), rotate your keys entirely below instead."] }), vault && vault.masterKeyWraps.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-2", children: [...passwordWraps, ...recoveryWraps, ...otherWraps].map((wrap) => {
|
|
804
|
+
(key.revocationReason === "superseded" ? (_jsx("span", { className: "text-text-muted", children: " (superseded)" })) : (_jsx("span", { className: "text-danger", children: " (revoked)" })))] }, key.fingerprint))) })) : (_jsx("p", { className: "text-sm text-text-muted", children: "No keys enrolled yet." }))] }), _jsx(ZeroAccessSetting, { mailbox: mailbox, canManage: canManageKeys }), _jsx(KeyExportSetting, { mailbox: mailbox, canManage: canManageKeys }), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Digital signatures" }), activeSigningKey && signingStatus !== "pending" && (_jsx("p", { className: ["text-sm text-text-muted", certificateMode ? "mb-3" : ""].join(" "), children: "Enabled \u2014 outgoing mail from this mailbox is signed with a publicly-trusted certificate." })), certificateMode && (_jsxs("div", { className: "flex flex-col gap-2", children: [certificateMode === "failed" && signingError && _jsx(Alert, { children: signingError }), _jsx(SigningCertificateCard, { mode: certificateMode, enrollment: certificateEnrollment, snapshot: enrollment, info: signingInfo, address: mailbox.primarySmtpAddress, keyNotAfter: certificateMode === "expired" ? expiredSigningKey?.notAfter : activeSigningKey?.notAfter, canRequest: canManageKeys, requesting: signingStatus === "enrolling", onCheck: canManageKeys ? () => void checkEnrollmentNow(mailboxUid) : undefined, onRequest: handleEnrollSigning })] })), !activeSigningKey && !certificateMode && !canManageKeys ? (_jsx("p", { className: "text-sm text-text-muted", children: "Not enabled." })) : activeSigningKey || certificateMode ? null : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-xs text-text-muted", children: "Lets recipients verify that mail from this mailbox is genuinely from you. Optional \u2014 encryption already works without it." }), beforeRequestText(signingInfo, mailbox.primarySmtpAddress) && (_jsx("p", { className: "text-xs text-text-muted", children: beforeRequestText(signingInfo, mailbox.primarySmtpAddress) })), healthWarning(signingInfo) && _jsx(Alert, { children: healthWarning(signingInfo) }), enrollment?.gone && _jsx(Alert, { children: STALE_REQUEST_TEXT }), signingError && _jsx(Alert, { children: signingError }), signingInfo?.backend !== "none" && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: signingStatus === "enrolling", disabled: signingStatus !== "idle", onClick: handleEnrollSigning, children: "Enable digital signatures" }))] }))] }), mailbox.escrowScopeId && (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Escrow" }), escrowError && _jsx(Alert, { children: escrowError }), hasEscrowWrap ? (_jsx("p", { className: "text-sm text-text-muted", children: "This mailbox is under legal/compliance escrow \u2014 an authorized holder in your organization can recover its encrypted mail if needed. This does not weaken protection against anyone else." })) : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("p", { className: "text-xs text-text-muted", children: ["Your organization has assigned this mailbox to an escrow scope, but nothing has been protected yet \u2014 an authorized holder cannot recover this mailbox\u2019s encrypted mail until you complete this step.", " ", "This does not weaken protection against anyone else."] }), canManageKeys && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: wrappingEscrow, disabled: wrappingEscrow, onClick: handleWrapEscrow, children: "Add escrow protection" }))] })), _jsxs(Modal, { open: pendingEscrow !== null, onClose: () => setPendingEscrow(null), title: "Confirm escrow certificate", children: [_jsx("p", { className: "text-sm mb-3", children: "Anyone holding the private key for this certificate will be able to recover this mailbox\u2019s encrypted mail. Confirm it is the escrow certificate your organization expects before continuing." }), _jsxs("dl", { className: "text-sm mb-5 flex flex-col gap-1", children: [_jsxs("div", { children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Escrow scope" }), _jsx("dd", { className: "break-all", children: pendingEscrow?.escrowScopeId })] }), _jsxs("div", { children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Certificate fingerprint" }), _jsx("dd", { className: "font-mono break-all", children: pendingEscrow?.publicKey.fingerprint })] })] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setPendingEscrow(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => void handleConfirmEscrow(pendingEscrow), children: "Trust this certificate" })] })] })] })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Unlock methods" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["Removing a method here stops it from being usable to unlock this mailbox going forward, but it is ", _jsx("strong", { children: "not" }), " full revocation \u2014 anyone who already captured a wrapped copy and knows its secret could still use it. For real revocation (e.g. after a lost device), rotate your keys entirely below instead."] }), vault && vault.masterKeyWraps.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-2", children: [...passwordWraps, ...recoveryWraps, ...otherWraps].map((wrap) => {
|
|
803
805
|
const key = `${wrap.method}:${wrap.methodId ?? ""}`;
|
|
804
806
|
return (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsx("span", { children: METHOD_LABELS[wrap.method] ?? wrap.method }), wrap.method !== "escrow" &&
|
|
805
807
|
canManageKeys &&
|
|
@@ -108,7 +108,7 @@ function ExportSection({ mailboxUid, mailboxLabel }) {
|
|
|
108
108
|
setCreating(false);
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
-
return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Export my data" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["Requests a copy of ", _jsx("strong", { children: mailboxLabel }), "\u2019s messages, contacts, calendar, tasks, and notes. This runs in the background and can take a few minutes \u2014 check back here for a download link once it\u2019s ready."] }), _jsxs("form", { onSubmit: handleCreate, className: "flex items-end gap-3 mb-5", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Format" }), _jsxs("select", { "aria-label": "Export format", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: format, onChange: (e) => setFormat(e.target.value), children: [_jsx("option", { value: "json", children: "JSON" }), _jsx("option", { value: "mbox", children: "Mbox" })] })] }), _jsx(Button, { type: "submit", variant: "secondary", className: "!w-auto", loading: creating, disabled: creating, children: "Request export" })] }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : requests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No export requests yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: requests.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [request.format.toUpperCase(), " \u00B7 ", new Date(request.dateCreated).toLocaleString(), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsxs("span", { className: "flex items-center gap-3", children: [_jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) }), request.status === "ready" && (_jsx("a", { href: exportRequestDownloadUrl(request.uid), className: "text-primary-dark hover:underline font-medium", children: "Download" }))] })] }, request.uid))) }))] }));
|
|
111
|
+
return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Export my data" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["Requests a copy of ", _jsx("strong", { children: mailboxLabel }), "\u2019s messages, contacts, calendar, tasks, and notes. This runs in the background and can take a few minutes \u2014 check back here for a download link once it\u2019s ready. Mail stored with zero-access encryption is exported encrypted: open it in a mail app that has your exported keys (Settings \u203A Encryption)."] }), _jsxs("form", { onSubmit: handleCreate, className: "flex items-end gap-3 mb-5", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Format" }), _jsxs("select", { "aria-label": "Export format", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: format, onChange: (e) => setFormat(e.target.value), children: [_jsx("option", { value: "json", children: "JSON" }), _jsx("option", { value: "mbox", children: "Mbox" })] })] }), _jsx(Button, { type: "submit", variant: "secondary", className: "!w-auto", loading: creating, disabled: creating, children: "Request export" })] }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : requests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No export requests yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: requests.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [request.format.toUpperCase(), " \u00B7 ", new Date(request.dateCreated).toLocaleString(), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsxs("span", { className: "flex items-center gap-3", children: [_jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) }), request.status === "ready" && (_jsx("a", { href: exportRequestDownloadUrl(request.uid), className: "text-primary-dark hover:underline font-medium", children: "Download" }))] })] }, request.uid))) }))] }));
|
|
112
112
|
}
|
|
113
113
|
function ImportSection({ mailboxUid }) {
|
|
114
114
|
const client = useApiClient();
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { ApiClient } from "../util/api.js";
|
|
11
11
|
import { ListParams, RequestListParams } from "../util/apiQuery.js";
|
|
12
|
+
import type { PublicKey, WrappedPrivateKey } from "../crypto/keyvaultApi.js";
|
|
12
13
|
export type { ListParams, RequestListParams };
|
|
13
14
|
export interface EscrowAccessRequestApproval {
|
|
14
15
|
holderUserUid: string;
|
|
@@ -54,6 +55,11 @@ export interface MasterKeyWrap {
|
|
|
54
55
|
* tooling, per `specs/end-to-end_encryption.md`'s "this server never holds the scope's private key". */
|
|
55
56
|
export interface EscrowAccessMaterial {
|
|
56
57
|
masterKeyWraps: MasterKeyWrap[];
|
|
58
|
+
/** The mailbox's wrapped private keys that read mail (zero-access and encryption keys, never signing), sealed under its master key. Absent
|
|
59
|
+
* from a server that predates releasing them. */
|
|
60
|
+
wrappedKeys?: WrappedPrivateKey[];
|
|
61
|
+
/** The certificate of each of `wrappedKeys`, matched by fingerprint. */
|
|
62
|
+
certificates?: PublicKey[];
|
|
57
63
|
}
|
|
58
64
|
/** `client`, given by every function below, is an explicit `ApiClient` from `createApiClient()` (e.g. one
|
|
59
65
|
* account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
|
|
@@ -112,6 +112,12 @@ export interface CalendarEvent {
|
|
|
112
112
|
* `mailto` only). Still to be rendered through `sanitizeEventDescriptionHtml()` (`eventDescription.ts`), never trusted as it stands. `null` or absent:
|
|
113
113
|
* the description, if any, is plain text. */
|
|
114
114
|
descriptionHtml?: string | null;
|
|
115
|
+
/** Response-only. An event made from an invitation that reached a zero-access mailbox: its title, location and description are only
|
|
116
|
+
* sealed to the mailbox's zero-access key (`title` is `[...]`). `apps/shared/calendar/sealedEvents.ts` opens them. */
|
|
117
|
+
sealedContent?: {
|
|
118
|
+
keyFingerprint: string;
|
|
119
|
+
sealed: string;
|
|
120
|
+
};
|
|
115
121
|
/** Who may see the event's details - absent (or `null`, on a row stored before this existed) reads as `"default"`; see `visibilityOf()`. */
|
|
116
122
|
visibility?: EventVisibility | null;
|
|
117
123
|
/** Whether the guests may ask for the event to change (default `false`). Absent or `null` reads as the default; see `guestPermissionsOf()`. */
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
* iTIP REPLY to the organizer. The client only reads the state and asks for a change; every call returns the invitation as
|
|
9
9
|
* it now stands, ready to draw. */
|
|
10
10
|
import { ApiRequestError, withClient } from "../util/api.js";
|
|
11
|
+
import { sealedInviteCalendar } from "./sealedInvites.js";
|
|
12
|
+
/** The calendar file to send for `messageUid` - a zero-access message this device decrypted (`sealedInvites.ts`) - as the request body's
|
|
13
|
+
* `ics`, or nothing for a message the server reads itself. */
|
|
14
|
+
function sealedSource(messageUid) {
|
|
15
|
+
const ics = sealedInviteCalendar(messageUid);
|
|
16
|
+
return ics !== undefined ? { ics } : {};
|
|
17
|
+
}
|
|
11
18
|
/** `client`, given by every function below, is an explicit `ApiClient` from `createApiClient()` (e.g. one
|
|
12
19
|
* account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
|
|
13
20
|
* own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
|
|
@@ -16,8 +23,15 @@ import { ApiRequestError, withClient } from "../util/api.js";
|
|
|
16
23
|
* common, expected answer for a message that merely has an attachment - and rejects for any other failure.
|
|
17
24
|
*/
|
|
18
25
|
export async function getMessageInvite(messageUid, client) {
|
|
26
|
+
const source = sealedSource(messageUid);
|
|
19
27
|
try {
|
|
20
|
-
|
|
28
|
+
// A zero-access message's invitation is read from the calendar file decrypted here (`POST .../view`).
|
|
29
|
+
return source.ics !== undefined
|
|
30
|
+
? await withClient(client, `/mail/calendar-events/invite/${encodeURIComponent(messageUid)}/view`, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
body: JSON.stringify(source),
|
|
33
|
+
})
|
|
34
|
+
: await withClient(client, `/mail/calendar-events/invite/${encodeURIComponent(messageUid)}`);
|
|
21
35
|
}
|
|
22
36
|
catch (err) {
|
|
23
37
|
if (err instanceof ApiRequestError && err.status === 404) {
|
|
@@ -34,21 +48,29 @@ export async function getMessageInvite(messageUid, client) {
|
|
|
34
48
|
export function respondToMessageInvite(messageUid, responseStatus, client) {
|
|
35
49
|
return withClient(client, `/mail/calendar-events/invite/${encodeURIComponent(messageUid)}/respond`, {
|
|
36
50
|
method: "POST",
|
|
37
|
-
body: JSON.stringify({ responseStatus }),
|
|
51
|
+
body: JSON.stringify({ responseStatus, ...sealedSource(messageUid) }),
|
|
38
52
|
});
|
|
39
53
|
}
|
|
40
54
|
/** Takes a canceled meeting off the reader's calendar. Resolves with the invitation as it now stands. */
|
|
41
55
|
export function removeMessageInvite(messageUid, client) {
|
|
42
|
-
|
|
56
|
+
const source = sealedSource(messageUid);
|
|
57
|
+
return withClient(client, `/mail/calendar-events/invite/${encodeURIComponent(messageUid)}/remove`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
...(source.ics !== undefined ? { body: JSON.stringify(source) } : {}),
|
|
60
|
+
});
|
|
43
61
|
}
|
|
44
62
|
/** Proposes a different time to the organizer of a `REQUEST` (an iTIP `COUNTER` sent by the server). Resolves with the invitation as it now stands. */
|
|
45
63
|
export function proposeNewTime(messageUid, proposal, client) {
|
|
46
64
|
return withClient(client, `/mail/calendar-events/invite/${encodeURIComponent(messageUid)}/propose`, {
|
|
47
65
|
method: "POST",
|
|
48
|
-
body: JSON.stringify(proposal),
|
|
66
|
+
body: JSON.stringify({ ...proposal, ...sealedSource(messageUid) }),
|
|
49
67
|
});
|
|
50
68
|
}
|
|
51
69
|
/** Accepts an attendee's proposed time (a `COUNTER`) for a meeting the reader organizes: the server moves the event and sends the attendees the update. Resolves with the invitation as it now stands. */
|
|
52
70
|
export function acceptProposal(messageUid, client) {
|
|
53
|
-
|
|
71
|
+
const source = sealedSource(messageUid);
|
|
72
|
+
return withClient(client, `/mail/calendar-events/invite/${encodeURIComponent(messageUid)}/accept-proposal`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
...(source.ics !== undefined ? { body: JSON.stringify(source) } : {}),
|
|
75
|
+
});
|
|
54
76
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The calendar files of zero-access messages, as decrypted on this device (`MessageSecurityResult.calendar`). The server holds only
|
|
3
|
+
* the sealed copy of such a message, so every invitation call for it (`inviteApi.ts`) sends the file along. Kept in memory for the
|
|
4
|
+
* messages read most recently, and cleared on sign-out (`clearSealedInvites()`).
|
|
5
|
+
*/
|
|
6
|
+
/** Remembers the decrypted calendar file of zero-access message `messageUid`. */
|
|
7
|
+
export declare function rememberSealedInvite(messageUid: string, ics: string): void;
|
|
8
|
+
/** The decrypted calendar file of zero-access message `messageUid`, when this device has read it. */
|
|
9
|
+
export declare function sealedInviteCalendar(messageUid: string): string | undefined;
|
|
10
|
+
/** Forgets every remembered calendar file (sign-out). */
|
|
11
|
+
export declare function clearSealedInvites(): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/**
|
|
6
|
+
* The calendar files of zero-access messages, as decrypted on this device (`MessageSecurityResult.calendar`). The server holds only
|
|
7
|
+
* the sealed copy of such a message, so every invitation call for it (`inviteApi.ts`) sends the file along. Kept in memory for the
|
|
8
|
+
* messages read most recently, and cleared on sign-out (`clearSealedInvites()`).
|
|
9
|
+
*/
|
|
10
|
+
/** How many messages' calendar files are kept. */
|
|
11
|
+
const MAX_SEALED_INVITES = 100;
|
|
12
|
+
const calendars = new Map();
|
|
13
|
+
/** Remembers the decrypted calendar file of zero-access message `messageUid`. */
|
|
14
|
+
export function rememberSealedInvite(messageUid, ics) {
|
|
15
|
+
calendars.delete(messageUid);
|
|
16
|
+
calendars.set(messageUid, ics);
|
|
17
|
+
while (calendars.size > MAX_SEALED_INVITES) {
|
|
18
|
+
calendars.delete(calendars.keys().next().value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** The decrypted calendar file of zero-access message `messageUid`, when this device has read it. */
|
|
22
|
+
export function sealedInviteCalendar(messageUid) {
|
|
23
|
+
return calendars.get(messageUid);
|
|
24
|
+
}
|
|
25
|
+
/** Forgets every remembered calendar file (sign-out). */
|
|
26
|
+
export function clearSealedInvites() {
|
|
27
|
+
calendars.clear();
|
|
28
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Export keys for Outlook/phone" (restapi `specs/search.md` §3, *Clients that don't run RapidMX*): a passphrase-protected
|
|
3
|
+
* PKCS#12 file holding the private keys a mail client needs to read this mailbox's mail on its own - every zero-access key
|
|
4
|
+
* (mail at rest is S/MIME `enveloped-data` to these) and every end-to-end encryption key (current and retained), plus, only
|
|
5
|
+
* when asked, the active signing key. Built entirely here from a fresh master-password unlock: the session's own private
|
|
6
|
+
* keys are non-extractable, and the server never sees the file or the passphrase. restapi only records that an export
|
|
7
|
+
* happened (`key_vault.export`), with the fingerprints exported and nothing else.
|
|
8
|
+
*/
|
|
9
|
+
import "reflect-metadata";
|
|
10
|
+
import type { ApiClient } from "../util/api.js";
|
|
11
|
+
import { type PublicKey } from "./keyvaultApi.js";
|
|
12
|
+
/** PBKDF2 iterations for the shrouded key bags and the PKCS#12 MAC. Windows, macOS and iOS all accept this. */
|
|
13
|
+
export declare const KEY_EXPORT_ITERATIONS = 100000;
|
|
14
|
+
/** The shortest export passphrase accepted. The file grants read access to all of this mailbox's mail. */
|
|
15
|
+
export declare const MIN_EXPORT_PASSPHRASE_LENGTH = 12;
|
|
16
|
+
/** Which key a bag holds, for its friendly name and the export's audit record. */
|
|
17
|
+
export type ExportedKeyKind = "zero-access" | "encrypt" | "sign";
|
|
18
|
+
/** What `exportKeysAsPkcs12()` takes. */
|
|
19
|
+
export interface KeyExportInput {
|
|
20
|
+
mailboxUid: string;
|
|
21
|
+
/** The mailbox's address, used in each key's friendly name. */
|
|
22
|
+
mailboxAddress: string;
|
|
23
|
+
/** The mailbox's published keys (`Mailbox.keys`): the encryption and signing certificates. */
|
|
24
|
+
mailboxKeys: PublicKey[];
|
|
25
|
+
/** The master password, for the fresh unlock every export needs. */
|
|
26
|
+
masterPassword: string;
|
|
27
|
+
/** Protects the PKCS#12 file. At least `MIN_EXPORT_PASSPHRASE_LENGTH` characters. */
|
|
28
|
+
exportPassphrase: string;
|
|
29
|
+
/** Whether to include the active signing key, for clients that will sign. Off unless the user opts in. */
|
|
30
|
+
includeSigningKey?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** One key in the export. */
|
|
33
|
+
export interface ExportedKey {
|
|
34
|
+
kind: ExportedKeyKind;
|
|
35
|
+
fingerprint: string;
|
|
36
|
+
}
|
|
37
|
+
/** What `exportKeysAsPkcs12()` resolves with. */
|
|
38
|
+
export interface KeyExport {
|
|
39
|
+
/** The DER-encoded PKCS#12 (`.p12`) file. */
|
|
40
|
+
pkcs12: Uint8Array;
|
|
41
|
+
/** The keys it holds, zero-access keys first. */
|
|
42
|
+
keys: ExportedKey[];
|
|
43
|
+
}
|
|
44
|
+
/** The export has nothing to hold - the vault has no zero-access or encryption private key this password opens. */
|
|
45
|
+
export declare class NothingToExportError extends Error {
|
|
46
|
+
constructor();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Builds the "Export keys for Outlook/phone" PKCS#12 for `input.mailboxUid` (see this module's comment) and records the
|
|
50
|
+
* export with restapi. Re-opens the vault with `masterPassword` - rejecting as `unlockWithPassword()` does on a wrong
|
|
51
|
+
* one - and never touches the in-memory session. Every opened private key and the master key are zeroed before this
|
|
52
|
+
* returns, whether or not it succeeds. Throws `NothingToExportError` when no key opens.
|
|
53
|
+
*/
|
|
54
|
+
export declare function exportKeysAsPkcs12(input: KeyExportInput, client?: ApiClient): Promise<KeyExport>;
|