@rapidmx/web-client 0.34.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/calendarLiveUpdates.ts +2 -5
- package/apps/shared/calendar/sealedEvents.ts +80 -0
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +61 -3
- package/apps/shared/components/contacts/ContactsSidebar.tsx +5 -1
- package/apps/shared/components/contacts/contactCardData.ts +8 -4
- 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 +91 -20
- package/apps/shared/components/mail/compose/draftResume.ts +28 -3
- package/apps/shared/components/mail/compose/quotedBody.ts +31 -15
- 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/MessageBody.tsx +4 -1
- package/apps/shared/components/mail/reading/bodyContent.ts +27 -17
- package/apps/shared/components/mail/reading/conversationPrint.ts +7 -5
- 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/live/useFolderLiveUpdates.ts +59 -0
- package/apps/shared/mail/decryptedMessages.ts +11 -3
- package/apps/shared/mail/outbox/sendJob.ts +2 -2
- package/apps/shared/mail/outbox/sendOutcomes.ts +1 -3
- 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 +180 -58
- package/apps/www/contacts/index.tsx +58 -2
- 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/apps/www/tasks/index.tsx +42 -0
- package/dist/apps/escrow/matters/[uid].js +2 -2
- package/dist/apps/shared/calendar/calendarLiveUpdates.js +2 -5
- 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/ContactsSidebar.d.ts +3 -1
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
- package/dist/apps/shared/components/contacts/contactCardData.d.ts +2 -2
- package/dist/apps/shared/components/contacts/contactCardData.js +8 -4
- 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 +73 -17
- 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 +4 -3
- package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -15
- 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/MessageBody.js +4 -1
- package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +4 -2
- package/dist/apps/shared/components/mail/reading/bodyContent.js +27 -16
- package/dist/apps/shared/components/mail/reading/conversationPrint.d.ts +3 -2
- package/dist/apps/shared/components/mail/reading/conversationPrint.js +7 -5
- 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/live/useFolderLiveUpdates.d.ts +19 -0
- package/dist/apps/shared/live/useFolderLiveUpdates.js +41 -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/outbox/sendOutcomes.js +1 -3
- 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 +48 -22
- package/dist/apps/www/contacts/index.js +49 -3
- 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/apps/www/tasks/index.js +29 -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/mail/pushClient.d.ts +10 -3
- package/dist/lib/mail/pushClient.js +18 -4
- package/dist/lib/search/zeroAccessIndexApi.d.ts +81 -0
- package/dist/lib/search/zeroAccessIndexApi.js +35 -0
- package/dist/lib/tasks/tasksApi.d.ts +1 -0
- package/dist/lib/tasks/tasksApi.js +3 -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/mail/pushClient.ts +18 -4
- package/lib/search/zeroAccessIndexApi.ts +119 -0
- package/lib/tasks/tasksApi.ts +4 -0
- package/package.json +3 -3
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { ApiClient, ApiRequestError } from "../util/api.js";
|
|
12
12
|
import { ListParams } from "../util/apiQuery.js";
|
|
13
|
-
import type { EncryptionPreference, PublicKey } from "../crypto/keyvaultApi.js";
|
|
13
|
+
import type { EncryptionPreference, PublicKey, ZeroAccessKey } from "../crypto/keyvaultApi.js";
|
|
14
14
|
import type { ResolvedPrincipal } from "./mailboxAccessApi.js";
|
|
15
15
|
export type { ListParams };
|
|
16
16
|
export type { EncryptionPreference, PublicKey };
|
|
@@ -95,6 +95,12 @@ export interface Mailbox {
|
|
|
95
95
|
* per the spec's own `PublicKey` doc comment. Absent/empty means no keys enrolled yet
|
|
96
96
|
* (`KeyEnrollmentGate` handles that state). */
|
|
97
97
|
keys?: PublicKey[];
|
|
98
|
+
/** The mailbox's zero-access certificates, managed by the server (never written by a client). Read them from
|
|
99
|
+
* `KeyVault.zeroAccessKeys` to unlock; here only so a settings page can tell whether one is enrolled. */
|
|
100
|
+
zeroAccessKeys?: ZeroAccessKey[];
|
|
101
|
+
/** "Enable zero-access encryption": unset means on. Only the mailbox's owner can change it - restapi drops anybody
|
|
102
|
+
* else's value. Has no effect while the server's `EncryptionPolicy.allowZeroAccess` is off. */
|
|
103
|
+
zeroAccessEnabled?: boolean;
|
|
98
104
|
/** How the caller reaches this mailbox: `"owner"` (theirs) or `"delegate"` (shared with them). Set by the plain list and read of
|
|
99
105
|
* mailboxes - not in an administration-scope answer - so a client can label the shared ones; see `isSharedWithMe()`. */
|
|
100
106
|
accessRole?: "owner" | "delegate";
|
|
@@ -215,6 +221,8 @@ export interface UpdateMailboxInput {
|
|
|
215
221
|
autoSendReceiptsInternal?: boolean;
|
|
216
222
|
autoSendReceiptsFederated?: boolean;
|
|
217
223
|
autoSendReceiptsExternal?: boolean;
|
|
224
|
+
/** Owner-only server-side (anybody else's value is dropped) - see `Mailbox.zeroAccessEnabled`. */
|
|
225
|
+
zeroAccessEnabled?: boolean;
|
|
218
226
|
/** Trusted-caller-only server-side. `null` (or `""`) unassigns the mailbox's escrow scope; the
|
|
219
227
|
* referenced `EscrowScope` must exist otherwise. See `Mailbox.escrowScopeId`. */
|
|
220
228
|
escrowScopeId?: string | null;
|
|
@@ -424,6 +432,13 @@ export interface Message {
|
|
|
424
432
|
* stored body, not from a flag this API exposes. Always present on a real `Message` (defaults
|
|
425
433
|
* `false` server-side), same optional-for-old-fixtures caveat as `Mailbox.oofEnabled`. */
|
|
426
434
|
encrypted?: boolean;
|
|
435
|
+
/** Set when the server sealed this message at rest on arrival (zero-access, restapi `specs/search.md` §3): its stored body is an
|
|
436
|
+
* S/MIME message only the mailbox's zero-access key opens, `subject` is `[...]` and `bodyPreview` empty - the real ones are in
|
|
437
|
+
* `sealedSummary`. */
|
|
438
|
+
zeroAccess?: {
|
|
439
|
+
keyFingerprint: string;
|
|
440
|
+
sealedSummary: string;
|
|
441
|
+
};
|
|
427
442
|
/** The raw RFC 2369/8058 `List-Unsubscribe` header value, if present — computed server-side by
|
|
428
443
|
* `@rapidmx/restapi`'s `ScanPipeline` from the stored MIME, the strongest single bulk-mail signal it
|
|
429
444
|
* captures. Used client-side to detect list traffic when replying, per
|
|
@@ -560,7 +575,7 @@ export type MessageReportKind = "junk" | "phishing" | "not_junk";
|
|
|
560
575
|
/** Why the server did not teach its spam filter from a report: `encrypted` (it has no plaintext to teach), `disabled` (learning is switched off),
|
|
561
576
|
* `unsupported` (no spam engine, or one that cannot learn), `too_large` (over the size it reads) or `failed` (the engine could not be reached,
|
|
562
577
|
* refused, or the message could not be read). The report itself still happened. */
|
|
563
|
-
export type MessageLearnSkipped = "encrypted" | "disabled" | "unsupported" | "too_large" | "failed";
|
|
578
|
+
export type MessageLearnSkipped = "encrypted" | "zero_access" | "disabled" | "unsupported" | "too_large" | "failed";
|
|
564
579
|
/** What `reportMessage()` answers. */
|
|
565
580
|
export interface MessageReportResult {
|
|
566
581
|
uid: string;
|
|
@@ -581,6 +596,9 @@ export interface ReportMessageOptions {
|
|
|
581
596
|
/** With `not_junk` only (else a 400): also add the message's From address to the mailbox's safe senders. Needs full access to the mailbox (403 otherwise,
|
|
582
597
|
* before anything is done). */
|
|
583
598
|
alwaysTrustSender?: boolean;
|
|
599
|
+
/** For a zero-access message only (else a 400): its original source, base64-encoded, decrypted here - the server holds only the sealed
|
|
600
|
+
* copy, so this is what its spam filter learns from (never stored). Without it the server skips learning (`learnSkipped: "zero_access"`). */
|
|
601
|
+
rawMessage?: string;
|
|
584
602
|
}
|
|
585
603
|
/**
|
|
586
604
|
* Reports a message as junk, phishing or not junk - `POST /mail/messages/:uid/report`. The SERVER moves it (`junk` and `phishing` to the mailbox's Junk Email,
|
|
@@ -708,7 +726,15 @@ export interface AssembleDraftInput {
|
|
|
708
726
|
bcc?: ComposeRecipientInput[];
|
|
709
727
|
subject?: string;
|
|
710
728
|
html: string;
|
|
729
|
+
/** See `AssembleIntent`. */
|
|
730
|
+
intent?: AssembleIntent;
|
|
711
731
|
}
|
|
732
|
+
/**
|
|
733
|
+
* Why a draft is being assembled (server `ComposeIntent`). `"save"`, the default, keeps an edit: in a zero-access mailbox the server
|
|
734
|
+
* stores it sealed, which it can't send. `"send"` is the assemble right before `sendMessage()`/`queueMessageSend()`: stored readable
|
|
735
|
+
* until it is relayed, then sealed as the Sent Items copy.
|
|
736
|
+
*/
|
|
737
|
+
export type AssembleIntent = "save" | "send";
|
|
712
738
|
/** What a draft records about the thread it belongs to - see `createDraft()`. */
|
|
713
739
|
export interface DraftThreading {
|
|
714
740
|
/** The `messageId` of the message being replied to (`Message.inReplyTo`). */
|
|
@@ -774,6 +800,8 @@ export interface AssembleDraftRawInput {
|
|
|
774
800
|
* `crypto/smimeMessage.ts`'s `buildSignedOnlyMessage()`/`buildEncryptedMessage()`, combined with
|
|
775
801
|
* the outer envelope headers (`From`/`To`/`Cc`/`Date`/`Message-ID`/`MIME-Version`) by the caller. */
|
|
776
802
|
rawMime: string;
|
|
803
|
+
/** See `AssembleIntent`. */
|
|
804
|
+
intent?: AssembleIntent;
|
|
777
805
|
}
|
|
778
806
|
/**
|
|
779
807
|
* Stores an already-signed/encrypted draft's raw MIME source as its `bodyBlobKey`, completely
|
package/dist/lib/mail/mailApi.js
CHANGED
|
@@ -274,7 +274,11 @@ export function moveMessage(message, folderUid, client) {
|
|
|
274
274
|
export function reportMessage(uid, kind, options = {}, client) {
|
|
275
275
|
return withClient(client, `/mail/messages/${encodeURIComponent(uid)}/report`, {
|
|
276
276
|
method: "POST",
|
|
277
|
-
body: JSON.stringify({
|
|
277
|
+
body: JSON.stringify({
|
|
278
|
+
kind,
|
|
279
|
+
...(options.alwaysTrustSender ? { alwaysTrustSender: true } : {}),
|
|
280
|
+
...(options.rawMessage ? { rawMessage: options.rawMessage } : {}),
|
|
281
|
+
}),
|
|
278
282
|
});
|
|
279
283
|
}
|
|
280
284
|
/**
|
|
@@ -28,6 +28,8 @@ export type PushStatus = "idle" | "connecting" | "open" | "reconnecting" | "clos
|
|
|
28
28
|
* caller orders its channels most important first.
|
|
29
29
|
*/
|
|
30
30
|
export declare const PUSH_MAX_CHANNELS = 40;
|
|
31
|
+
/** The group `setChannels()` sets when no other is named: Mail's. */
|
|
32
|
+
export declare const DEFAULT_CHANNEL_GROUP = "mail";
|
|
31
33
|
/** The first reconnect waits about this long, and each further failure doubles it ... */
|
|
32
34
|
export declare const PUSH_BACKOFF_BASE_MS = 1000;
|
|
33
35
|
/** ... up to this. */
|
|
@@ -55,6 +57,8 @@ export declare class PushClient {
|
|
|
55
57
|
private current;
|
|
56
58
|
/** What the caller wants to hear about, most important first. */
|
|
57
59
|
private desired;
|
|
60
|
+
/** The channels each caller asked for (`setChannels()`'s `group`), merged into `desired`. */
|
|
61
|
+
private groups;
|
|
58
62
|
/** What this socket is subscribed to (including whatever the server put it on at connect). */
|
|
59
63
|
private subscribed;
|
|
60
64
|
/** The channels this client itself asked for and was granted - the only ones it may unsubscribe. */
|
|
@@ -73,10 +77,13 @@ export declare class PushClient {
|
|
|
73
77
|
*/
|
|
74
78
|
close(): void;
|
|
75
79
|
/**
|
|
76
|
-
* Sets the channels (folder and mailbox uids)
|
|
77
|
-
*
|
|
80
|
+
* Sets the channels (folder and mailbox uids) one caller wants events for: Mail's, in the default group, or a page's
|
|
81
|
+
* own (the Calendar's calendars - Mail subscribes to mail folders only). Every group's channels are wanted together,
|
|
82
|
+
* each page's own group ahead of the default one, since a page's are what is on screen; deduplicated and cut to
|
|
83
|
+
* `PUSH_MAX_CHANNELS`. An empty list drops the group. On an open socket the difference is subscribed/unsubscribed at
|
|
84
|
+
* once, otherwise it is sent when the socket opens.
|
|
78
85
|
*/
|
|
79
|
-
setChannels(channels: readonly string[]): void;
|
|
86
|
+
setChannels(channels: readonly string[], group?: string): void;
|
|
80
87
|
/** Calls `listener` with every event; returns the function that stops it. */
|
|
81
88
|
onEvent(listener: (event: PushEvent) => void): () => void;
|
|
82
89
|
/** Calls `listener` whenever `status` changes; returns the function that stops it. */
|
|
@@ -37,6 +37,8 @@ const SOCKET_OPEN = 1;
|
|
|
37
37
|
* caller orders its channels most important first.
|
|
38
38
|
*/
|
|
39
39
|
export const PUSH_MAX_CHANNELS = 40;
|
|
40
|
+
/** The group `setChannels()` sets when no other is named: Mail's. */
|
|
41
|
+
export const DEFAULT_CHANNEL_GROUP = "mail";
|
|
40
42
|
/** The first reconnect waits about this long, and each further failure doubles it ... */
|
|
41
43
|
export const PUSH_BACKOFF_BASE_MS = 1000;
|
|
42
44
|
/** ... up to this. */
|
|
@@ -61,6 +63,8 @@ export class PushClient {
|
|
|
61
63
|
this.current = "idle";
|
|
62
64
|
/** What the caller wants to hear about, most important first. */
|
|
63
65
|
this.desired = [];
|
|
66
|
+
/** The channels each caller asked for (`setChannels()`'s `group`), merged into `desired`. */
|
|
67
|
+
this.groups = new Map();
|
|
64
68
|
/** What this socket is subscribed to (including whatever the server put it on at connect). */
|
|
65
69
|
this.subscribed = new Set();
|
|
66
70
|
/** The channels this client itself asked for and was granted - the only ones it may unsubscribe. */
|
|
@@ -107,11 +111,21 @@ export class PushClient {
|
|
|
107
111
|
this.setStatus("closed");
|
|
108
112
|
}
|
|
109
113
|
/**
|
|
110
|
-
* Sets the channels (folder and mailbox uids)
|
|
111
|
-
*
|
|
114
|
+
* Sets the channels (folder and mailbox uids) one caller wants events for: Mail's, in the default group, or a page's
|
|
115
|
+
* own (the Calendar's calendars - Mail subscribes to mail folders only). Every group's channels are wanted together,
|
|
116
|
+
* each page's own group ahead of the default one, since a page's are what is on screen; deduplicated and cut to
|
|
117
|
+
* `PUSH_MAX_CHANNELS`. An empty list drops the group. On an open socket the difference is subscribed/unsubscribed at
|
|
118
|
+
* once, otherwise it is sent when the socket opens.
|
|
112
119
|
*/
|
|
113
|
-
setChannels(channels) {
|
|
114
|
-
|
|
120
|
+
setChannels(channels, group = DEFAULT_CHANNEL_GROUP) {
|
|
121
|
+
if (channels.length > 0) {
|
|
122
|
+
this.groups.set(group, [...channels]);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
this.groups.delete(group);
|
|
126
|
+
}
|
|
127
|
+
const ordered = [...this.groups.entries()].sort(([a], [b]) => Number(a === DEFAULT_CHANNEL_GROUP) - Number(b === DEFAULT_CHANNEL_GROUP));
|
|
128
|
+
this.desired = [...new Set(ordered.flatMap(([, list]) => list))].slice(0, PUSH_MAX_CHANNELS);
|
|
115
129
|
if (this.ready) {
|
|
116
130
|
this.syncSubscriptions();
|
|
117
131
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed wrappers over `@rapidmx/restapi`'s zero-access search index (`/mail/mailboxes/:id/search-index`, `specs/search.md` §7-8): the
|
|
3
|
+
* sealed index entries the server writes at delivery and the weekly segments clients compact them into. Everything here is
|
|
4
|
+
* ciphertext encrypted to the mailbox's zero-access key - `apps/shared/search/zeroAccessIndexSync.ts` decrypts and imports it.
|
|
5
|
+
*/
|
|
6
|
+
import { type ApiClient } from "../util/api.js";
|
|
7
|
+
export type SegmentOrigin = "zero-access" | "client";
|
|
8
|
+
/** A compacted segment as the manifest lists it. */
|
|
9
|
+
export interface IndexSegment {
|
|
10
|
+
weekId: string;
|
|
11
|
+
generation: number;
|
|
12
|
+
origin: SegmentOrigin;
|
|
13
|
+
bytes: number;
|
|
14
|
+
etag: string;
|
|
15
|
+
schemaVersion: number;
|
|
16
|
+
keyFingerprint: string;
|
|
17
|
+
entryCount: number;
|
|
18
|
+
}
|
|
19
|
+
/** A week's entries not yet compacted. */
|
|
20
|
+
export interface IndexWeekEntries {
|
|
21
|
+
weekId: string;
|
|
22
|
+
pendingEntries: number;
|
|
23
|
+
latestEntryAt: string;
|
|
24
|
+
/** The week has closed and its grace period has passed: a client may compact it. */
|
|
25
|
+
compactable: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface SegmentManifest {
|
|
28
|
+
mailboxUid: string;
|
|
29
|
+
segments: IndexSegment[];
|
|
30
|
+
weeks: IndexWeekEntries[];
|
|
31
|
+
}
|
|
32
|
+
/** One pending entry: `sealed` is base64 CMS `EnvelopedData` of a gzipped JSON `ZeroAccessIndexDocument`. */
|
|
33
|
+
export interface SealedIndexEntry {
|
|
34
|
+
uid: string;
|
|
35
|
+
weekId: string;
|
|
36
|
+
messageUid: string;
|
|
37
|
+
createdAt: string;
|
|
38
|
+
keyFingerprint: string;
|
|
39
|
+
sealed: string;
|
|
40
|
+
}
|
|
41
|
+
export interface SealedIndexEntryPage {
|
|
42
|
+
entries: SealedIndexEntry[];
|
|
43
|
+
next?: string;
|
|
44
|
+
}
|
|
45
|
+
/** What a sealed entry (and each document of a segment) decrypts to - restapi's `ZeroAccessIndexDocument`. */
|
|
46
|
+
export interface ZeroAccessIndexDocument {
|
|
47
|
+
v: 1;
|
|
48
|
+
messageUid: string;
|
|
49
|
+
subject: string;
|
|
50
|
+
body: string;
|
|
51
|
+
attachmentText: string[];
|
|
52
|
+
attachmentNames: string[];
|
|
53
|
+
deferredAttachments: string[];
|
|
54
|
+
from: string;
|
|
55
|
+
to: string[];
|
|
56
|
+
cc: string[];
|
|
57
|
+
participants: string[];
|
|
58
|
+
dateForSort: number;
|
|
59
|
+
}
|
|
60
|
+
/** `PUT .../segments/:weekId`. */
|
|
61
|
+
export interface SegmentUpload {
|
|
62
|
+
expectedGeneration: number;
|
|
63
|
+
/** Base64 of the sealed file. */
|
|
64
|
+
segment: string;
|
|
65
|
+
keyFingerprint: string;
|
|
66
|
+
schemaVersion: number;
|
|
67
|
+
origin?: SegmentOrigin;
|
|
68
|
+
foldedEntryUids?: string[];
|
|
69
|
+
}
|
|
70
|
+
export declare function getSearchIndexManifest(mailboxUid: string, client?: ApiClient): Promise<SegmentManifest>;
|
|
71
|
+
/** One page of pending entries: of `week` when given, written at or after `since` when given, after uid `after`. */
|
|
72
|
+
export declare function listSearchIndexEntries(mailboxUid: string, options?: {
|
|
73
|
+
week?: string;
|
|
74
|
+
since?: string;
|
|
75
|
+
after?: string;
|
|
76
|
+
limit?: number;
|
|
77
|
+
}, client?: ApiClient): Promise<SealedIndexEntryPage>;
|
|
78
|
+
/** The sealed file of a week's segment generation. */
|
|
79
|
+
export declare function getSearchIndexSegment(mailboxUid: string, segment: Pick<IndexSegment, "weekId" | "generation" | "origin">, client?: ApiClient): Promise<Uint8Array>;
|
|
80
|
+
/** Stores a compaction. Rejects with a `409` when the week's segment changed since `upload.expectedGeneration` was read. */
|
|
81
|
+
export declare function putSearchIndexSegment(mailboxUid: string, weekId: string, upload: SegmentUpload, client?: ApiClient): Promise<IndexSegment>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/**
|
|
6
|
+
* Typed wrappers over `@rapidmx/restapi`'s zero-access search index (`/mail/mailboxes/:id/search-index`, `specs/search.md` §7-8): the
|
|
7
|
+
* sealed index entries the server writes at delivery and the weekly segments clients compact them into. Everything here is
|
|
8
|
+
* ciphertext encrypted to the mailbox's zero-access key - `apps/shared/search/zeroAccessIndexSync.ts` decrypts and imports it.
|
|
9
|
+
*/
|
|
10
|
+
import { withClient, withClientBlob } from "../util/api.js";
|
|
11
|
+
const base = (mailboxUid) => `/mail/mailboxes/${encodeURIComponent(mailboxUid)}/search-index`;
|
|
12
|
+
export function getSearchIndexManifest(mailboxUid, client) {
|
|
13
|
+
return withClient(client, `${base(mailboxUid)}/manifest`);
|
|
14
|
+
}
|
|
15
|
+
/** One page of pending entries: of `week` when given, written at or after `since` when given, after uid `after`. */
|
|
16
|
+
export function listSearchIndexEntries(mailboxUid, options = {}, client) {
|
|
17
|
+
const query = new URLSearchParams();
|
|
18
|
+
for (const [key, value] of Object.entries(options)) {
|
|
19
|
+
if (value !== undefined) {
|
|
20
|
+
query.set(key, String(value));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const search = query.toString();
|
|
24
|
+
return withClient(client, `${base(mailboxUid)}/entries${search ? `?${search}` : ""}`);
|
|
25
|
+
}
|
|
26
|
+
/** The sealed file of a week's segment generation. */
|
|
27
|
+
export async function getSearchIndexSegment(mailboxUid, segment, client) {
|
|
28
|
+
const query = `generation=${segment.generation}${segment.origin === "client" ? "&origin=client" : ""}`;
|
|
29
|
+
const blob = await withClientBlob(client, `${base(mailboxUid)}/segments/${encodeURIComponent(segment.weekId)}?${query}`, "Could not download a search index segment.");
|
|
30
|
+
return new Uint8Array(await blob.arrayBuffer());
|
|
31
|
+
}
|
|
32
|
+
/** Stores a compaction. Rejects with a `409` when the week's segment changed since `upload.expectedGeneration` was read. */
|
|
33
|
+
export function putSearchIndexSegment(mailboxUid, weekId, upload, client) {
|
|
34
|
+
return withClient(client, `${base(mailboxUid)}/segments/${encodeURIComponent(weekId)}`, { method: "PUT", body: JSON.stringify(upload) });
|
|
35
|
+
}
|
|
@@ -30,6 +30,7 @@ export interface Task {
|
|
|
30
30
|
* own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
|
|
31
31
|
/** Lists a folder's tasks, soonest due date first (tasks with no due date sort last). */
|
|
32
32
|
export declare function listTasks(folderUid: string, params?: ListParams, client?: ApiClient): Promise<Task[]>;
|
|
33
|
+
export declare function getTask(uid: string, client?: ApiClient): Promise<Task>;
|
|
33
34
|
export interface CreateTaskInput {
|
|
34
35
|
mailboxUid: string;
|
|
35
36
|
folderUid: string;
|
|
@@ -13,6 +13,9 @@ import { SETTINGS_LIST_LIMIT, buildQuery } from "../util/apiQuery.js";
|
|
|
13
13
|
export function listTasks(folderUid, params = {}, client) {
|
|
14
14
|
return withClient(client, `/mail/tasks?${buildQuery(params, { folderUid, sort: JSON.stringify({ dueDate: "ASC", uid: "ASC" }) })}`);
|
|
15
15
|
}
|
|
16
|
+
export function getTask(uid, client) {
|
|
17
|
+
return withClient(client, `/mail/tasks/${encodeURIComponent(uid)}`);
|
|
18
|
+
}
|
|
16
19
|
export function createTask(input, client) {
|
|
17
20
|
return withClient(client, "/mail/tasks", {
|
|
18
21
|
method: "POST",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { ApiClient, withClient } from "../util/api.js";
|
|
16
16
|
import { ListParams, RequestListParams, buildQuery } from "../util/apiQuery.js";
|
|
17
|
+
import type { PublicKey, WrappedPrivateKey } from "../crypto/keyvaultApi.js";
|
|
17
18
|
|
|
18
19
|
export type { ListParams, RequestListParams };
|
|
19
20
|
|
|
@@ -65,6 +66,11 @@ export interface MasterKeyWrap {
|
|
|
65
66
|
* tooling, per `specs/end-to-end_encryption.md`'s "this server never holds the scope's private key". */
|
|
66
67
|
export interface EscrowAccessMaterial {
|
|
67
68
|
masterKeyWraps: MasterKeyWrap[];
|
|
69
|
+
/** The mailbox's wrapped private keys that read mail (zero-access and encryption keys, never signing), sealed under its master key. Absent
|
|
70
|
+
* from a server that predates releasing them. */
|
|
71
|
+
wrappedKeys?: WrappedPrivateKey[];
|
|
72
|
+
/** The certificate of each of `wrappedKeys`, matched by fingerprint. */
|
|
73
|
+
certificates?: PublicKey[];
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
/** `client`, given by every function below, is an explicit `ApiClient` from `createApiClient()` (e.g. one
|
|
@@ -135,6 +135,9 @@ export interface CalendarEvent {
|
|
|
135
135
|
* `mailto` only). Still to be rendered through `sanitizeEventDescriptionHtml()` (`eventDescription.ts`), never trusted as it stands. `null` or absent:
|
|
136
136
|
* the description, if any, is plain text. */
|
|
137
137
|
descriptionHtml?: string | null;
|
|
138
|
+
/** Response-only. An event made from an invitation that reached a zero-access mailbox: its title, location and description are only
|
|
139
|
+
* sealed to the mailbox's zero-access key (`title` is `[...]`). `apps/shared/calendar/sealedEvents.ts` opens them. */
|
|
140
|
+
sealedContent?: { keyFingerprint: string; sealed: string };
|
|
138
141
|
/** Who may see the event's details - absent (or `null`, on a row stored before this existed) reads as `"default"`; see `visibilityOf()`. */
|
|
139
142
|
visibility?: EventVisibility | null;
|
|
140
143
|
/** Whether the guests may ask for the event to change (default `false`). Absent or `null` reads as the default; see `guestPermissionsOf()`. */
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
|
|
11
11
|
import { ApiClient, ApiRequestError, withClient } from "../util/api.js";
|
|
12
12
|
import type { EventVisibility, GuestPermissions } from "./calendarApi.js";
|
|
13
|
+
import { sealedInviteCalendar } from "./sealedInvites.js";
|
|
14
|
+
|
|
15
|
+
/** The calendar file to send for `messageUid` - a zero-access message this device decrypted (`sealedInvites.ts`) - as the request body's
|
|
16
|
+
* `ics`, or nothing for a message the server reads itself. */
|
|
17
|
+
function sealedSource(messageUid: string): { ics?: string } {
|
|
18
|
+
const ics = sealedInviteCalendar(messageUid);
|
|
19
|
+
return ics !== undefined ? { ics } : {};
|
|
20
|
+
}
|
|
13
21
|
|
|
14
22
|
/** What the reader can answer an invitation with. */
|
|
15
23
|
export type InviteResponse = "accepted" | "tentative" | "declined";
|
|
@@ -104,8 +112,15 @@ export interface MessageInvite {
|
|
|
104
112
|
* common, expected answer for a message that merely has an attachment - and rejects for any other failure.
|
|
105
113
|
*/
|
|
106
114
|
export async function getMessageInvite(messageUid: string, client?: ApiClient): Promise<MessageInvite | null> {
|
|
115
|
+
const source = sealedSource(messageUid);
|
|
107
116
|
try {
|
|
108
|
-
|
|
117
|
+
// A zero-access message's invitation is read from the calendar file decrypted here (`POST .../view`).
|
|
118
|
+
return source.ics !== undefined
|
|
119
|
+
? await withClient<MessageInvite>(client, `/mail/calendar-events/invite/${encodeURIComponent(messageUid)}/view`, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
body: JSON.stringify(source),
|
|
122
|
+
})
|
|
123
|
+
: await withClient<MessageInvite>(client, `/mail/calendar-events/invite/${encodeURIComponent(messageUid)}`);
|
|
109
124
|
} catch (err) {
|
|
110
125
|
if (err instanceof ApiRequestError && err.status === 404) {
|
|
111
126
|
return null;
|
|
@@ -122,13 +137,17 @@ export async function getMessageInvite(messageUid: string, client?: ApiClient):
|
|
|
122
137
|
export function respondToMessageInvite(messageUid: string, responseStatus: InviteResponse, client?: ApiClient): Promise<MessageInvite> {
|
|
123
138
|
return withClient(client, `/mail/calendar-events/invite/${encodeURIComponent(messageUid)}/respond`, {
|
|
124
139
|
method: "POST",
|
|
125
|
-
body: JSON.stringify({ responseStatus }),
|
|
140
|
+
body: JSON.stringify({ responseStatus, ...sealedSource(messageUid) }),
|
|
126
141
|
});
|
|
127
142
|
}
|
|
128
143
|
|
|
129
144
|
/** Takes a canceled meeting off the reader's calendar. Resolves with the invitation as it now stands. */
|
|
130
145
|
export function removeMessageInvite(messageUid: string, client?: ApiClient): Promise<MessageInvite> {
|
|
131
|
-
|
|
146
|
+
const source = sealedSource(messageUid);
|
|
147
|
+
return withClient(client, `/mail/calendar-events/invite/${encodeURIComponent(messageUid)}/remove`, {
|
|
148
|
+
method: "POST",
|
|
149
|
+
...(source.ics !== undefined ? { body: JSON.stringify(source) } : {}),
|
|
150
|
+
});
|
|
132
151
|
}
|
|
133
152
|
|
|
134
153
|
/** A different time to suggest to the organizer. */
|
|
@@ -144,11 +163,15 @@ export interface ProposedTime {
|
|
|
144
163
|
export function proposeNewTime(messageUid: string, proposal: ProposedTime, client?: ApiClient): Promise<MessageInvite> {
|
|
145
164
|
return withClient(client, `/mail/calendar-events/invite/${encodeURIComponent(messageUid)}/propose`, {
|
|
146
165
|
method: "POST",
|
|
147
|
-
body: JSON.stringify(proposal),
|
|
166
|
+
body: JSON.stringify({ ...proposal, ...sealedSource(messageUid) }),
|
|
148
167
|
});
|
|
149
168
|
}
|
|
150
169
|
|
|
151
170
|
/** 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. */
|
|
152
171
|
export function acceptProposal(messageUid: string, client?: ApiClient): Promise<MessageInvite> {
|
|
153
|
-
|
|
172
|
+
const source = sealedSource(messageUid);
|
|
173
|
+
return withClient(client, `/mail/calendar-events/invite/${encodeURIComponent(messageUid)}/accept-proposal`, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
...(source.ics !== undefined ? { body: JSON.stringify(source) } : {}),
|
|
176
|
+
});
|
|
154
177
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
|
|
11
|
+
/** How many messages' calendar files are kept. */
|
|
12
|
+
const MAX_SEALED_INVITES = 100;
|
|
13
|
+
|
|
14
|
+
const calendars: Map<string, string> = new Map();
|
|
15
|
+
|
|
16
|
+
/** Remembers the decrypted calendar file of zero-access message `messageUid`. */
|
|
17
|
+
export function rememberSealedInvite(messageUid: string, ics: string): void {
|
|
18
|
+
calendars.delete(messageUid);
|
|
19
|
+
calendars.set(messageUid, ics);
|
|
20
|
+
while (calendars.size > MAX_SEALED_INVITES) {
|
|
21
|
+
calendars.delete(calendars.keys().next().value!);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The decrypted calendar file of zero-access message `messageUid`, when this device has read it. */
|
|
26
|
+
export function sealedInviteCalendar(messageUid: string): string | undefined {
|
|
27
|
+
return calendars.get(messageUid);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Forgets every remembered calendar file (sign-out). */
|
|
31
|
+
export function clearSealedInvites(): void {
|
|
32
|
+
calendars.clear();
|
|
33
|
+
}
|