@rapidmx/web-client 0.5.0 → 0.7.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/README.md +88 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +25 -8
- package/apps/shared/components/admin/settings/BrandingForm.tsx +398 -385
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -112
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +159 -152
- package/apps/shared/components/admin/settings/PluginsManager.tsx +1288 -1235
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +9 -1
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -290
- package/apps/shared/components/admin/setup/SetupWizard.tsx +443 -416
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +194 -192
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +199 -197
- package/apps/shared/components/layout/AppShell.tsx +288 -270
- package/apps/shared/components/mail/ConversationList.tsx +225 -33
- package/apps/shared/components/mail/ConversationThreadPane.tsx +434 -216
- package/apps/shared/components/mail/MailListToolbar.tsx +265 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +222 -0
- package/apps/shared/components/mail/MenuButton.tsx +384 -0
- package/apps/shared/components/mail/MessageDetailPane.tsx +280 -215
- package/apps/shared/components/mail/MoveToFolderDialog.tsx +263 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +29 -2
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1700 -1652
- package/apps/shared/components/mail/compose/RecipientInput.tsx +356 -0
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +38 -3
- package/apps/shared/components/mail/compose/quotedBody.ts +100 -0
- package/apps/shared/components/mail/compose/recipients.ts +104 -0
- package/apps/shared/components/mail/labelMenu.tsx +278 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +31 -4
- package/apps/shared/components/mail/listPreferences.ts +228 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +23 -10
- package/apps/shared/components/tasks/layout/TasksShell.tsx +201 -199
- package/apps/shared/plugins/pluginNav.ts +73 -0
- package/apps/shared/search/localIndexRpcClient.ts +3 -1
- package/apps/shared/styles/app.css +28 -5
- package/apps/www/index.tsx +1854 -1314
- package/apps/www/messages/[uid].tsx +5 -4
- package/dist/apps/admin/_404.d.ts +2 -0
- package/dist/apps/admin/_500.d.ts +2 -0
- package/dist/apps/admin/_layout.d.ts +8 -0
- package/dist/apps/admin/audit-log/index.d.ts +11 -0
- package/dist/apps/admin/branding/index.d.ts +3 -0
- package/dist/apps/admin/data-requests/index.d.ts +3 -0
- package/dist/apps/admin/distribution-lists/[uid].d.ts +7 -0
- package/dist/apps/admin/distribution-lists/index.d.ts +3 -0
- package/dist/apps/admin/distribution-lists/new/index.d.ts +3 -0
- package/dist/apps/admin/domains/[uid].d.ts +7 -0
- package/dist/apps/admin/domains/index.d.ts +3 -0
- package/dist/apps/admin/domains/new/index.d.ts +3 -0
- package/dist/apps/admin/encryption-policy/index.d.ts +3 -0
- package/dist/apps/admin/escrow-scopes/[uid].d.ts +21 -0
- package/dist/apps/admin/escrow-scopes/index.d.ts +3 -0
- package/dist/apps/admin/escrow-scopes/new/index.d.ts +3 -0
- package/dist/apps/admin/index.d.ts +3 -0
- package/dist/apps/admin/ingest-queue/index.d.ts +4 -0
- package/dist/apps/admin/mailbox-policy/index.d.ts +3 -0
- package/dist/apps/admin/mailboxes/[uid].d.ts +7 -0
- package/dist/apps/admin/mailboxes/new/index.d.ts +3 -0
- package/dist/apps/admin/plugins/index.d.ts +3 -0
- package/dist/apps/admin/quarantine/index.d.ts +6 -0
- package/dist/apps/admin/retention-policy/index.d.ts +3 -0
- package/dist/apps/admin/setup/index.d.ts +3 -0
- package/dist/apps/admin/transport-rules/[uid].d.ts +7 -0
- package/dist/apps/admin/transport-rules/_transportRuleConfig.d.ts +21 -0
- package/dist/apps/admin/transport-rules/index.d.ts +3 -0
- package/dist/apps/admin/transport-rules/new/index.d.ts +3 -0
- package/dist/apps/escrow/_layout.d.ts +10 -0
- package/dist/apps/escrow/audit-log/index.d.ts +9 -0
- package/dist/apps/escrow/index.d.ts +3 -0
- package/dist/apps/escrow/matters/[uid].d.ts +7 -0
- package/dist/apps/escrow/matters/new/index.d.ts +3 -0
- package/dist/apps/shared/components/admin/distributionLists/MemberListCard.d.ts +17 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +32 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +32 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.js +12 -5
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.d.ts +12 -0
- package/dist/apps/shared/components/admin/mailboxes/MailboxTable.d.ts +6 -0
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.d.ts +19 -0
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +12 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +13 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +6 -5
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +12 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.d.ts +15 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +2 -2
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.d.ts +9 -0
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.d.ts +19 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +13 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +2 -2
- package/dist/apps/shared/components/admin/settings/PluginsManager.d.ts +11 -0
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +33 -14
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.d.ts +12 -0
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +2 -2
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.d.ts +14 -0
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +1 -1
- package/dist/apps/shared/components/admin/setup/SetupWizard.d.ts +36 -0
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +14 -10
- package/dist/apps/shared/components/admin/signOut.d.ts +23 -0
- package/dist/apps/shared/components/admin/usePagedList.d.ts +39 -0
- package/dist/apps/shared/components/calendar/CalendarListSidebar.d.ts +26 -0
- package/dist/apps/shared/components/calendar/EventModal.d.ts +46 -0
- package/dist/apps/shared/components/calendar/MonthView.d.ts +15 -0
- package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +20 -0
- package/dist/apps/shared/components/calendar/ResourcePicker.d.ts +19 -0
- package/dist/apps/shared/components/calendar/SplitDayView.d.ts +28 -0
- package/dist/apps/shared/components/calendar/TimeGridView.d.ts +17 -0
- package/dist/apps/shared/components/calendar/allDay.d.ts +51 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +40 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +2 -2
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +23 -0
- package/dist/apps/shared/components/contacts/ContactForm.d.ts +20 -0
- package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +40 -0
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +25 -0
- package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +40 -0
- package/dist/apps/shared/components/contacts/contactKeys.d.ts +29 -0
- package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +21 -0
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +2 -2
- package/dist/apps/shared/components/escrow/layout/EscrowShell.d.ts +25 -0
- package/dist/apps/shared/components/forms/StringListField.d.ts +22 -0
- package/dist/apps/shared/components/layout/AppShell.d.ts +49 -0
- package/dist/apps/shared/components/layout/AppShell.js +15 -13
- package/dist/apps/shared/components/layout/BrandingChrome.d.ts +18 -0
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.d.ts +51 -0
- package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +13 -0
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +66 -0
- package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +45 -0
- package/dist/apps/shared/components/layout/UserMenu.d.ts +23 -0
- package/dist/apps/shared/components/mail/ConversationList.d.ts +47 -0
- package/dist/apps/shared/components/mail/ConversationList.js +84 -10
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +62 -0
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +217 -60
- package/dist/apps/shared/components/mail/MailListToolbar.d.ts +55 -0
- package/dist/apps/shared/components/mail/MailListToolbar.js +149 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +59 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.js +48 -0
- package/dist/apps/shared/components/mail/MenuButton.d.ts +71 -0
- package/dist/apps/shared/components/mail/MenuButton.js +211 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +101 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +126 -129
- package/dist/apps/shared/components/mail/MoveToFolderDialog.d.ts +57 -0
- package/dist/apps/shared/components/mail/MoveToFolderDialog.js +138 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +73 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +3 -1
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +15 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +37 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +52 -20
- package/dist/apps/shared/components/mail/compose/EmojiPicker.d.ts +14 -0
- package/dist/apps/shared/components/mail/compose/GifPicker.d.ts +13 -0
- package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +36 -0
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +203 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +43 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.js +21 -3
- package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.d.ts +16 -0
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +27 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +37 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +85 -0
- package/dist/apps/shared/components/mail/compose/recipients.d.ts +24 -0
- package/dist/apps/shared/components/mail/compose/recipients.js +87 -0
- package/dist/apps/shared/components/mail/labelMenu.d.ts +97 -0
- package/dist/apps/shared/components/mail/labelMenu.js +134 -0
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +65 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +17 -5
- package/dist/apps/shared/components/mail/listPreferences.d.ts +101 -0
- package/dist/apps/shared/components/mail/listPreferences.js +186 -0
- package/dist/apps/shared/components/mail/pinnedSigners.d.ts +33 -0
- package/dist/apps/shared/components/mail/verificationSeals.d.ts +22 -0
- package/dist/apps/shared/components/mail/writableMailboxes.d.ts +15 -0
- package/dist/apps/shared/components/rules/RuleBuilder.d.ts +70 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +41 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +19 -10
- package/dist/apps/shared/components/tasks/TasksSidebar.d.ts +41 -0
- package/dist/apps/shared/components/tasks/TasksToolbar.d.ts +14 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +24 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +2 -2
- package/dist/apps/shared/mail/findWellKnownFolderUid.d.ts +5 -0
- package/dist/apps/shared/mail/listAllPages.d.ts +17 -0
- package/dist/apps/shared/plugins/pluginNav.d.ts +39 -0
- package/dist/apps/shared/plugins/pluginNav.js +33 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +13 -0
- package/dist/apps/shared/search/localIndexBlockCipher.d.ts +36 -0
- package/dist/apps/shared/search/localIndexBuilder.d.ts +85 -0
- package/dist/apps/shared/search/localIndexKey.d.ts +4 -0
- package/dist/apps/shared/search/localIndexRpcClient.d.ts +71 -0
- package/dist/apps/shared/search/localIndexRpcClient.js +3 -1
- package/dist/apps/shared/search/localIndexSchema.d.ts +111 -0
- package/dist/apps/shared/search/localIndexSizePreference.d.ts +27 -0
- package/dist/apps/shared/search/localIndexStorage.d.ts +28 -0
- package/dist/apps/shared/search/localIndexVFS.d.ts +101 -0
- package/dist/apps/shared/search/localIndexWorker.d.ts +145 -0
- package/dist/apps/shared/search/searchTier2.d.ts +30 -0
- package/dist/apps/shared/styles/app.css +28 -5
- package/dist/apps/www/_404.d.ts +2 -0
- package/dist/apps/www/_500.d.ts +2 -0
- package/dist/apps/www/_layout.d.ts +14 -0
- package/dist/apps/www/calendar/index.d.ts +3 -0
- package/dist/apps/www/contacts/[uid].d.ts +13 -0
- package/dist/apps/www/contacts/index.d.ts +3 -0
- package/dist/apps/www/index.d.ts +3 -0
- package/dist/apps/www/index.js +506 -112
- package/dist/apps/www/messages/[uid].d.ts +11 -0
- package/dist/apps/www/messages/[uid].js +2 -3
- package/dist/apps/www/settings/auto-reply/index.d.ts +4 -0
- package/dist/apps/www/settings/encryption/index.d.ts +7 -0
- package/dist/apps/www/settings/filters/[uid].d.ts +8 -0
- package/dist/apps/www/settings/filters/_mailFilterRuleConfig.d.ts +11 -0
- package/dist/apps/www/settings/filters/index.d.ts +4 -0
- package/dist/apps/www/settings/filters/new/index.d.ts +4 -0
- package/dist/apps/www/settings/labels/index.d.ts +4 -0
- package/dist/apps/www/settings/privacy/index.d.ts +4 -0
- package/dist/apps/www/settings/read-receipts/index.d.ts +4 -0
- package/dist/apps/www/settings/sharing/index.d.ts +4 -0
- package/dist/apps/www/settings/signatures/[uid].d.ts +8 -0
- package/dist/apps/www/settings/signatures/index.d.ts +4 -0
- package/dist/apps/www/settings/signatures/new/index.d.ts +4 -0
- package/dist/apps/www/settings/signatures/signatureDefaults.d.ts +10 -0
- package/dist/apps/www/tasks/index.d.ts +3 -0
- package/package.json +3 -3
- package/apps/shared/components/booking/AvailabilityEditor.tsx +0 -118
- package/apps/www/settings/booking-types/[uid].tsx +0 -306
- package/apps/www/settings/booking-types/index.tsx +0 -98
- package/apps/www/settings/booking-types/new/index.tsx +0 -212
- package/apps/www/settings/focused-inbox/index.tsx +0 -150
- package/dist/apps/shared/components/booking/AvailabilityEditor.js +0 -45
- package/dist/apps/www/settings/booking-types/[uid].js +0 -134
- package/dist/apps/www/settings/booking-types/index.js +0 -29
- package/dist/apps/www/settings/booking-types/new/index.js +0 -86
- package/dist/apps/www/settings/focused-inbox/index.js +0 -71
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The trusted signing-key fingerprints for a received message's sender, for `evaluateMessageSecurity()`'s
|
|
3
|
+
* `pinnedSignerFingerprints`: the keys key discovery already pinned on the contacts in the reading mailbox's own
|
|
4
|
+
* contacts folder(s). Reads only stored contacts - never triggers a Discovery lookup, which must not happen on
|
|
5
|
+
* message receipt.
|
|
6
|
+
*
|
|
7
|
+
* `MessageDetailPane` renders once per opened message (and once per message in a thread), so each mailbox's
|
|
8
|
+
* contact list is fetched once and reused for `CONTACTS_CACHE_TTL_MS`; a failed load isn't cached. Any failure
|
|
9
|
+
* leaves the caller with no pins, which `evaluateMessageSecurity()` reports as an unverified signer - never as
|
|
10
|
+
* verified.
|
|
11
|
+
*
|
|
12
|
+
* The cache is dropped (`clearPinnedSignerCache()`) whenever a key session locks (subscribed on first use), on sign-out
|
|
13
|
+
* (`AppShell`), and after the reader adds, edits or deletes contacts (the contacts pages) - so a contact removed
|
|
14
|
+
* because its key was revoked doesn't keep vouching for signatures until the TTL runs out.
|
|
15
|
+
*/
|
|
16
|
+
import { SignerKeyState } from "@rapidmx/react-shared/contacts/contactsApi.js";
|
|
17
|
+
/** How long one mailbox's loaded contact list is reused before it is fetched again. */
|
|
18
|
+
export declare const CONTACTS_CACHE_TTL_MS = 60000;
|
|
19
|
+
/** Forgets every cached contact list - on sign-out, a key-session lock, after the reader changes their contacts, and
|
|
20
|
+
* in tests. */
|
|
21
|
+
export declare function clearPinnedSignerCache(): void;
|
|
22
|
+
/** The sender `address`'s pinned signing fingerprints from `mailboxUid`'s contacts. Rejects if they can't be loaded. */
|
|
23
|
+
export declare function getPinnedSignerFingerprints(mailboxUid: string, address: string): Promise<string[]>;
|
|
24
|
+
/** A sender's signing-key state (`signerKeyStateFor()`) plus what a key-changed notice needs to show and link. */
|
|
25
|
+
export interface SenderKeyState extends SignerKeyState {
|
|
26
|
+
/** The contact holding the first pinned signing key, else the one holding the conflict, else the first match. */
|
|
27
|
+
contactUid?: string;
|
|
28
|
+
/** When the first pinned signing key started being trusted - see `keyPinnedSince()`. */
|
|
29
|
+
pinnedSince?: number;
|
|
30
|
+
}
|
|
31
|
+
/** The sender `address`'s signing-key state from `mailboxUid`'s (cached) contacts, for a "signing key changed" comparison
|
|
32
|
+
* or to spot a recorded conflict. Reads stored contacts only. Rejects if they can't be loaded. */
|
|
33
|
+
export declare function getSignerKeyState(mailboxUid: string, address: string): Promise<SenderKeyState>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Message } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
2
|
+
import type { SealToWrite } from "@rapidmx/react-shared/crypto/messageSecurity.js";
|
|
3
|
+
/** How long one mailbox's vault generation is reused before it is read again. */
|
|
4
|
+
export declare const VAULT_GENERATION_TTL_MS = 60000;
|
|
5
|
+
/** Forgets the cached vault generations and this session's seal writes - in tests. */
|
|
6
|
+
export declare function clearVerificationSealCache(): void;
|
|
7
|
+
/** Reads `mailboxUid`'s current vault `masterKeyGeneration`, uncached: `undefined` when the vault can't be read or
|
|
8
|
+
* reports none. Never rejects. */
|
|
9
|
+
export declare function readVaultGeneration(mailboxUid: string): Promise<number | undefined>;
|
|
10
|
+
/** `mailboxUid`'s current vault `masterKeyGeneration`, or `undefined` when it can't be read. Never rejects. */
|
|
11
|
+
export declare function getVaultGeneration(mailboxUid: string): Promise<number | undefined>;
|
|
12
|
+
/** The seal to check `message` against: the one this session stored for it when that is newer than the message's own
|
|
13
|
+
* copy (which may predate the write), else the message's `verificationSeal`/`verificationSealGeneration`. */
|
|
14
|
+
export declare function currentVerificationSeal(message: Pick<Message, "uid" | "verificationSeal" | "verificationSealGeneration">): {
|
|
15
|
+
seal?: string;
|
|
16
|
+
sealGeneration?: number;
|
|
17
|
+
};
|
|
18
|
+
/** Whether `sendVerificationSeal()` would still send a seal for `messageUid` at `generation`. */
|
|
19
|
+
export declare function verificationSealPending(messageUid: string, generation: number): boolean;
|
|
20
|
+
/** Stores `sealToWrite` on `messageUid`, best effort - see this module's doc comment. Resolves `true` when a request was
|
|
21
|
+
* sent, `false` when the write was skipped as already attempted. Never rejects. */
|
|
22
|
+
export declare function sendVerificationSeal(messageUid: string, sealToWrite: SealToWrite): Promise<boolean>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
2
|
+
/** `true` writable, `false` known view-only, `undefined` couldn't tell (treated as writable). */
|
|
3
|
+
export type MailboxWritability = boolean | undefined;
|
|
4
|
+
/** Forgets every cached answer - for tests, and anything that changes the caller's access mid-session. */
|
|
5
|
+
export declare function clearMailboxWritabilityCache(): void;
|
|
6
|
+
/** The already-known answer for `mailbox`, without making a request. */
|
|
7
|
+
export declare function peekMailboxWritability(mailbox: Mailbox, userUid: string | undefined, trusted?: boolean): MailboxWritability;
|
|
8
|
+
/** Whether the caller can create items in `mailbox`. Never rejects. */
|
|
9
|
+
export declare function getMailboxWritability(mailbox: Mailbox, userUid: string | undefined, trusted?: boolean): Promise<MailboxWritability>;
|
|
10
|
+
/** Per-mailbox writability answers for `mailboxes`, filled in as each one arrives (already-known answers are
|
|
11
|
+
* present from the first render). */
|
|
12
|
+
export declare function useMailboxWritability(mailboxes: Mailbox[], userUid: string | undefined, trusted?: boolean): Record<string, MailboxWritability>;
|
|
13
|
+
/** `mailboxes` minus any known to be view-only, always keeping `keepUid` (the picker's current/default value,
|
|
14
|
+
* so the control never shows a value missing from its own options). Lists everything until told otherwise. */
|
|
15
|
+
export declare function useWritableMailboxes(mailboxes: Mailbox[], userUid: string | undefined, keepUid?: string, trusted?: boolean): Mailbox[];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
/** One condition field this scope's `Conditions` shape supports — `"list"` renders an add/remove chip
|
|
3
|
+
* editor for a `string[]` field, `"boolean"` a plain checkbox, `"select"` a fixed-choice dropdown for a
|
|
4
|
+
* field with a small closed set of string values (e.g. `MailFilterConditions.importance`). */
|
|
5
|
+
export interface ConditionFieldDef {
|
|
6
|
+
key: string;
|
|
7
|
+
label: string;
|
|
8
|
+
kind: "list" | "boolean" | "select";
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
/** Required when `kind` is `"select"` — the fixed set of values this field may hold, plus an
|
|
11
|
+
* implicit "Any" option that clears the field entirely. */
|
|
12
|
+
options?: {
|
|
13
|
+
value: string;
|
|
14
|
+
label: string;
|
|
15
|
+
}[];
|
|
16
|
+
}
|
|
17
|
+
/** One action type this scope's `Action` union supports — the per-scope registry the shared condition
|
|
18
|
+
* editor/chrome below is deliberately kept out of (transport-rule and mail-filter actions are
|
|
19
|
+
* different enough vocabularies that one component special-casing both would be worse than this). */
|
|
20
|
+
export interface ActionTypeDef<A> {
|
|
21
|
+
value: string;
|
|
22
|
+
label: string;
|
|
23
|
+
/** Builds a fresh default action of this type when picked from "Add action". */
|
|
24
|
+
createDefault: () => A;
|
|
25
|
+
/** Renders this action's own type-specific fields (e.g. header name/value). `null` for an action
|
|
26
|
+
* type with nothing further to configure. */
|
|
27
|
+
render: (action: A, onChange: (next: A) => void) => ReactNode;
|
|
28
|
+
}
|
|
29
|
+
export interface RuleBuilderValue<C extends object, A extends {
|
|
30
|
+
type: string;
|
|
31
|
+
}> {
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
sequence: number;
|
|
34
|
+
stopProcessingRules: boolean;
|
|
35
|
+
conditions: C;
|
|
36
|
+
actions: A[];
|
|
37
|
+
}
|
|
38
|
+
export interface RuleBuilderProps<C extends object, A extends {
|
|
39
|
+
type: string;
|
|
40
|
+
}> {
|
|
41
|
+
value: RuleBuilderValue<C, A>;
|
|
42
|
+
onChange: (next: RuleBuilderValue<C, A>) => void;
|
|
43
|
+
conditionFields: ConditionFieldDef[];
|
|
44
|
+
actionTypes: ActionTypeDef<A>[];
|
|
45
|
+
/** Set by the page once the user has tried to save a rule `hasConditions()` rejects - turns the
|
|
46
|
+
* "add at least one condition" hint into an error. */
|
|
47
|
+
showValidation?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Whether `conditions` holds at least one real condition: a list with a non-blank entry, a ticked
|
|
51
|
+
* boolean, or a chosen select value. A rule with none would match *every* message, so the pages that save
|
|
52
|
+
* a `RuleBuilder` value (mail filters, transport rules) must refuse to save unless this returns `true`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function hasConditions(conditions: object): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Shared condition-row editor and enabled/sequence/stop-processing chrome for a rule that matches
|
|
57
|
+
* conditions and runs actions in order — `TransportRule` (admin, org-wide) and `MailFilterRule`
|
|
58
|
+
* (mailbox-scoped) both have this same `{enabled, sequence, stopProcessingRules, conditions, actions}`
|
|
59
|
+
* shape. Conditions are driven by a declarative `conditionFields` list so both scopes share this one
|
|
60
|
+
* editor despite their `Conditions` types not being identical — `MailFilterConditions.importance`
|
|
61
|
+
* (`MailFilterRule`'s one condition field `TransportRuleConditions` has no equivalent of) is what forced
|
|
62
|
+
* this list beyond `"list"`/`"boolean"` to add a third `"select"` kind for a small fixed-choice field,
|
|
63
|
+
* confirming the shared editor still generalizes rather than needing a fork; actions are driven by a
|
|
64
|
+
* small per-scope `actionTypes` registry instead, since the two action vocabularies (reject/quarantine/
|
|
65
|
+
* add-header/add-recipient vs. move/copy/delete/mark-read/forward) are different enough that one
|
|
66
|
+
* component special-casing both would be worse than this split.
|
|
67
|
+
*/
|
|
68
|
+
export default function RuleBuilder<C extends object, A extends {
|
|
69
|
+
type: string;
|
|
70
|
+
}>({ value, onChange, conditionFields, actionTypes, showValidation, }: RuleBuilderProps<C, A>): React.JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React, { PropsWithChildren } from "react";
|
|
2
|
+
import { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
3
|
+
import { AppShellProps } from "../../layout/AppShell.js";
|
|
4
|
+
import { PluginNav } from "../../../plugins/pluginNav.js";
|
|
5
|
+
export interface SettingsSectionDef {
|
|
6
|
+
id: string;
|
|
7
|
+
href: string;
|
|
8
|
+
label: string;
|
|
9
|
+
}
|
|
10
|
+
/** Every settings section with its own sidebar entry. */
|
|
11
|
+
export declare const SETTINGS_SECTIONS: SettingsSectionDef[];
|
|
12
|
+
/** A `SETTINGS_SECTIONS` id, or a plugin's `settingsSections` item id (see `PluginNav`). */
|
|
13
|
+
export type SettingsSectionId = string;
|
|
14
|
+
/** `SETTINGS_SECTIONS` followed by the plugins' `settingsSections` items, core ids winning - see
|
|
15
|
+
* `mergePluginNavItems`. */
|
|
16
|
+
export declare function settingsSections(pluginNav?: PluginNav): SettingsSectionDef[];
|
|
17
|
+
export interface SettingsShellProps extends Omit<AppShellProps, "active"> {
|
|
18
|
+
/** Which entry in `SETTINGS_SECTIONS` (or plugin `settingsSections` item) is highlighted in this shell's own sidebar — distinct from
|
|
19
|
+
* `AppShell`'s own `active` (always hardcoded to `"settings"` below), since Settings isn't a rail
|
|
20
|
+
* icon (see `AppShell.tsx`'s `AppShellApp` doc comment) but still needs its own section
|
|
21
|
+
* highlighting. Each settings page passes its own section id explicitly, the same "gotcha" this
|
|
22
|
+
* plan already calls out for every shell's `active` prop. */
|
|
23
|
+
active: SettingsSectionId;
|
|
24
|
+
}
|
|
25
|
+
export interface SettingsShellContextValue {
|
|
26
|
+
/** The mailbox currently selected (`?mailboxUid=`, or the caller's first accessible mailbox) —
|
|
27
|
+
* every settings section so far (Automatic Replies) edits mailbox-scoped fields. */
|
|
28
|
+
mailboxUid?: string;
|
|
29
|
+
mailboxes: Mailbox[];
|
|
30
|
+
}
|
|
31
|
+
/** Reads the mailbox Settings is currently scoped to, as resolved by the enclosing `SettingsShell`. */
|
|
32
|
+
export declare function useSettingsShell(): SettingsShellContextValue;
|
|
33
|
+
/**
|
|
34
|
+
* The Settings area's shell — reached via a `UserMenu` entry rather than a 5th `AppShell` icon (per
|
|
35
|
+
* this plan's own confirmed decision), so it always passes `active="settings"` to `AppShell` regardless
|
|
36
|
+
* of which section is showing. Unlike `ContactsShell`/`TasksShell` (whose sidebar is just a mailbox
|
|
37
|
+
* switcher, since each has exactly one well-known folder), this shell's sidebar is a real list of
|
|
38
|
+
* settings sections (`SETTINGS_SECTIONS`) — full-page navigation between them, same as the top-level
|
|
39
|
+
* app rail, since this framework has no client-side router.
|
|
40
|
+
*/
|
|
41
|
+
export default function SettingsShell({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }: PropsWithChildren<SettingsShellProps>): React.JSX.Element;
|
|
@@ -12,19 +12,27 @@ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
|
12
12
|
import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
13
13
|
import AppShell from "../../layout/AppShell.js";
|
|
14
14
|
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
15
|
+
import { mergePluginNavItems } from "../../../plugins/pluginNav.js";
|
|
15
16
|
/** Every settings section with its own sidebar entry. */
|
|
16
17
|
export const SETTINGS_SECTIONS = [
|
|
17
18
|
{ id: "auto-reply", href: "/settings/auto-reply", label: "Automatic Replies" },
|
|
18
19
|
{ id: "filters", href: "/settings/filters", label: "Mail Filters" },
|
|
19
20
|
{ id: "signatures", href: "/settings/signatures", label: "Signatures" },
|
|
20
21
|
{ id: "labels", href: "/settings/labels", label: "Labels" },
|
|
21
|
-
{ id: "focused-inbox", href: "/settings/focused-inbox", label: "Focused Inbox" },
|
|
22
22
|
{ id: "read-receipts", href: "/settings/read-receipts", label: "Read Receipts" },
|
|
23
|
-
{ id: "booking-types", href: "/settings/booking-types", label: "Booking Links" },
|
|
24
23
|
{ id: "encryption", href: "/settings/encryption", label: "Encryption" },
|
|
25
24
|
{ id: "sharing", href: "/settings/sharing", label: "Sharing" },
|
|
26
25
|
{ id: "privacy", href: "/settings/privacy", label: "Privacy & Data" },
|
|
27
26
|
];
|
|
27
|
+
/** `SETTINGS_SECTIONS` followed by the plugins' `settingsSections` items, core ids winning - see
|
|
28
|
+
* `mergePluginNavItems`. */
|
|
29
|
+
export function settingsSections(pluginNav) {
|
|
30
|
+
return mergePluginNavItems(SETTINGS_SECTIONS, pluginNav?.settingsSections, ({ id, href, label }) => ({
|
|
31
|
+
id,
|
|
32
|
+
href,
|
|
33
|
+
label,
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
28
36
|
const SettingsShellContext = createContext({ mailboxes: [] });
|
|
29
37
|
/** Reads the mailbox Settings is currently scoped to, as resolved by the enclosing `SettingsShell`. */
|
|
30
38
|
export function useSettingsShell() {
|
|
@@ -38,7 +46,7 @@ export function useSettingsShell() {
|
|
|
38
46
|
* settings sections (`SETTINGS_SECTIONS`) — full-page navigation between them, same as the top-level
|
|
39
47
|
* app rail, since this framework has no client-side router.
|
|
40
48
|
*/
|
|
41
|
-
export default function SettingsShell({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, children, }) {
|
|
49
|
+
export default function SettingsShell({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }) {
|
|
42
50
|
const [status, setStatus] = useState("checking");
|
|
43
51
|
const [error, setError] = useState(null);
|
|
44
52
|
const [mailboxes, setMailboxes] = useState([]);
|
|
@@ -77,16 +85,17 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
77
85
|
inner = (_jsx("div", { className: "flex-1 flex items-center justify-center p-8", children: _jsx("div", { className: "w-full max-w-md", children: _jsx(Alert, { children: error }) }) }));
|
|
78
86
|
}
|
|
79
87
|
else if (userUid && status === "ready") {
|
|
88
|
+
const sections = settingsSections(pluginNav);
|
|
80
89
|
// A function, not a plain JSX constant — same reasoning as ContactsShell's identical comment:
|
|
81
90
|
// rendered twice (desktop `<aside>` + mobile `Drawer`), possibly simultaneously mounted, so the
|
|
82
91
|
// `<select>`'s `id`/its `<label>`'s `htmlFor` need a distinct value per instance.
|
|
83
92
|
const sidebarContent = (idPrefix) => (_jsxs(_Fragment, { children: [mailboxes.length > 1 && (_jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold uppercase tracking-wide text-text-muted mb-1", htmlFor: `${idPrefix}-settings-mailbox-switcher`, children: "Mailbox" }), _jsx("select", { id: `${idPrefix}-settings-mailbox-switcher`, className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: mailboxUid, onChange: (e) => {
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
const
|
|
88
|
-
window.location.href = `${
|
|
89
|
-
}, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), _jsx("nav", { "aria-label": "Settings sections", className: "flex flex-col gap-0.5", children:
|
|
93
|
+
// Every core settings page passes its own section id (see
|
|
94
|
+
// `SettingsShellProps.active`), but a plugin page's section is missing when the
|
|
95
|
+
// server didn't send its nav item, so that falls back to the current path.
|
|
96
|
+
const href = sections.find((s) => s.id === active)?.href ?? window.location.pathname;
|
|
97
|
+
window.location.href = `${href}?mailboxUid=${encodeURIComponent(e.target.value)}`;
|
|
98
|
+
}, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), _jsx("nav", { "aria-label": "Settings sections", className: "flex flex-col gap-0.5", children: sections.map((section) => (_jsx("a", {
|
|
90
99
|
// `inner` (this whole branch) is only ever computed once `status === "ready"`
|
|
91
100
|
// and `mailboxUid` has already resolved truthy — a falsy `mailboxUid` returns
|
|
92
101
|
// `<MailboxProvisioning />` above instead (and TypeScript's own narrowing of
|
|
@@ -100,5 +109,5 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
100
109
|
].join(" "), children: section.label }, section.id))) })] }));
|
|
101
110
|
inner = (_jsxs(_Fragment, { children: [_jsx("aside", { className: "hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Settings", children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [_jsx("button", { type: "button", className: "md:hidden m-3 self-start w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open settings menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx(SettingsShellContext.Provider, { value: contextValue, children: children })] })] }));
|
|
102
111
|
}
|
|
103
|
-
return (_jsx(AppShell, { active: "settings", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, children: inner }));
|
|
112
|
+
return (_jsx(AppShell, { active: "settings", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, pluginNav: pluginNav, children: inner }));
|
|
104
113
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Task } from "@rapidmx/react-shared/tasks/tasksApi.js";
|
|
3
|
+
export type TasksView = {
|
|
4
|
+
type: "myDay";
|
|
5
|
+
} | {
|
|
6
|
+
type: "important";
|
|
7
|
+
} | {
|
|
8
|
+
type: "planned";
|
|
9
|
+
} | {
|
|
10
|
+
type: "assignedToMe";
|
|
11
|
+
} | {
|
|
12
|
+
type: "flagged";
|
|
13
|
+
} | {
|
|
14
|
+
type: "all";
|
|
15
|
+
} | {
|
|
16
|
+
type: "list";
|
|
17
|
+
uid: string;
|
|
18
|
+
name: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function tasksViewKey(view: TasksView): string;
|
|
21
|
+
export interface TasksSidebarProps {
|
|
22
|
+
mailboxUid?: string;
|
|
23
|
+
/** The caller's own (incomplete) tasks — used to compute every smart-filter's count. Deliberately not
|
|
24
|
+
* fetched again here; the parent already has this loaded for the main list. */
|
|
25
|
+
tasks: Task[];
|
|
26
|
+
userUid?: string;
|
|
27
|
+
active: TasksView;
|
|
28
|
+
onSelect: (view: TasksView) => void;
|
|
29
|
+
/** Bumped by the parent to force a contact-list-style re-fetch of task lists (mirrors
|
|
30
|
+
* `ContactsSidebar`'s identical prop) — not currently used by any caller, kept for parity/future use. */
|
|
31
|
+
refreshToken?: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Tasks' Outlook To-Do-style left sidebar. "My Day"/"Assigned to me" are real persisted fields
|
|
35
|
+
* (`Task.myDay`/`assignedTo`, from Phase 0); "Important"/"Planned" are pure smart-filters derived from
|
|
36
|
+
* already-loaded fields (`priority === "high"`/`dueDate != null`) with no backend concept of their own;
|
|
37
|
+
* "Flagged email" is a `Message` smart-filter (see `flaggedMessages.ts`), not a `Task` one at all — the
|
|
38
|
+
* page renders it as a distinct list, not the task table. "Tasks" is the flat, unfiltered default. Custom
|
|
39
|
+
* lists are real `TaskList` records, fetched here (same pattern as `ContactsSidebar`'s contact lists).
|
|
40
|
+
*/
|
|
41
|
+
export default function TasksSidebar({ mailboxUid, tasks, userUid, active, onSelect, refreshToken }: TasksSidebarProps): React.JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export type TasksViewMode = "list" | "grid";
|
|
3
|
+
export interface TasksToolbarProps {
|
|
4
|
+
viewMode: TasksViewMode;
|
|
5
|
+
onViewModeChange: (mode: TasksViewMode) => void;
|
|
6
|
+
selectedCount: number;
|
|
7
|
+
onComplete: () => void;
|
|
8
|
+
onDelete: () => void;
|
|
9
|
+
onAddToMyDay: () => void;
|
|
10
|
+
}
|
|
11
|
+
/** Tasks' toolbar: a Grid/List view-mode toggle (local, not persisted) plus bulk actions for whatever
|
|
12
|
+
* rows are checked (Complete, Add to My Day, Delete) — deliberately presentational, matching
|
|
13
|
+
* `ContactsToolbar`'s convention, since the page owns the actual task data/selection. */
|
|
14
|
+
export default function TasksToolbar({ viewMode, onViewModeChange, selectedCount, onComplete, onDelete, onAddToMyDay }: TasksToolbarProps): React.JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React, { PropsWithChildren } from "react";
|
|
2
|
+
import { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
3
|
+
import { AppShellProps } from "../../layout/AppShell.js";
|
|
4
|
+
export type TasksShellProps = Omit<AppShellProps, "active">;
|
|
5
|
+
export interface TasksShellContextValue {
|
|
6
|
+
/** The mailbox currently selected (`?mailboxUid=`, or the caller's first accessible mailbox). */
|
|
7
|
+
mailboxUid?: string;
|
|
8
|
+
/** The selected mailbox's single `tasks`-type folder. */
|
|
9
|
+
folderUid?: string;
|
|
10
|
+
mailboxes: Mailbox[];
|
|
11
|
+
/** The caller's own uid — exposed here (rather than only as `AppShell`'s prop) so the page content
|
|
12
|
+
* can compute "assigned to me" without needing it threaded through separately. */
|
|
13
|
+
userUid?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Reads the mailbox/folder Tasks is currently showing, as resolved by the enclosing `TasksShell`. */
|
|
16
|
+
export declare function useTasksShell(): TasksShellContextValue;
|
|
17
|
+
/**
|
|
18
|
+
* The Tasks app's shell — structurally identical to `ContactsShell` (a mailbox has exactly one
|
|
19
|
+
* well-known `tasks` folder, guaranteed to exist by `BaseMailboxRoute.create()`'s eager provisioning
|
|
20
|
+
* in `@rapidmx/restapi`), just bound to a different folder type. Kept as its own small component
|
|
21
|
+
* (rather than a single generic "SingleFolderShell" parameterized by type) so each app's sidebar can
|
|
22
|
+
* grow its own app-specific controls later without threading extra props through a shared one.
|
|
23
|
+
*/
|
|
24
|
+
export default function TasksShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }: PropsWithChildren<TasksShellProps>): React.JSX.Element;
|
|
@@ -24,7 +24,7 @@ export function useTasksShell() {
|
|
|
24
24
|
* (rather than a single generic "SingleFolderShell" parameterized by type) so each app's sidebar can
|
|
25
25
|
* grow its own app-specific controls later without threading extra props through a shared one.
|
|
26
26
|
*/
|
|
27
|
-
export default function TasksShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, children, }) {
|
|
27
|
+
export default function TasksShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }) {
|
|
28
28
|
const [status, setStatus] = useState("checking");
|
|
29
29
|
const [error, setError] = useState(null);
|
|
30
30
|
const [mailboxes, setMailboxes] = useState([]);
|
|
@@ -88,5 +88,5 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
|
|
|
88
88
|
}, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), folderError && _jsx(Alert, { children: folderError })] }));
|
|
89
89
|
inner = (_jsxs(_Fragment, { children: [_jsx("aside", { className: "hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Mailbox", children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [(mailboxes.length > 1 || folderError) && (_jsx("button", { type: "button", className: "md:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open mailbox switcher", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) })), _jsx(TasksShellContext.Provider, { value: contextValue, children: children })] })] }));
|
|
90
90
|
}
|
|
91
|
-
return (_jsx(AppShell, { active: "tasks", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, children: inner }));
|
|
91
|
+
return (_jsx(AppShell, { active: "tasks", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, pluginNav: pluginNav, children: inner }));
|
|
92
92
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { FolderType } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
2
|
+
/** The uid of a mailbox's well-known folder of `type` (e.g. its Contacts or Tasks folder), for creating an
|
|
3
|
+
* item in a mailbox other than the one an app currently has loaded. Resolves `undefined` if the mailbox has
|
|
4
|
+
* no such folder; rejects if the folder list can't be fetched. */
|
|
5
|
+
export declare function findWellKnownFolderUid(mailboxUid: string, type: FolderType): Promise<string | undefined>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Page size the list pages (Contacts, Tasks) request per call - restapi caps `limit` at 500. */
|
|
2
|
+
export declare const LIST_PAGE_SIZE = 500;
|
|
3
|
+
/** Safety cap on pages fetched - stops a server that ignores `page` (and keeps returning a full first
|
|
4
|
+
* page) from looping forever. 40 pages x 500 = 20,000 items. */
|
|
5
|
+
export declare const MAX_LIST_PAGES = 40;
|
|
6
|
+
export interface ListAllPagesResult<T> {
|
|
7
|
+
items: T[];
|
|
8
|
+
/** `true` when fetching stopped at `maxPages` with the last page still full - there may be more items
|
|
9
|
+
* the caller isn't showing, so it should say so rather than presenting `items` as the whole list. */
|
|
10
|
+
truncated: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Fetches every page of a zero-based `limit`/`page` list endpoint, stopping at the first short page
|
|
14
|
+
* (fewer than `pageSize` items) - so a folder with more items than one page holds is shown in full
|
|
15
|
+
* rather than silently cut off at the first page. Stops at `maxPages` regardless, reporting `truncated`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function listAllPages<T>(fetchPage: (page: number) => Promise<T[]>, pageSize?: number, maxPages?: number): Promise<ListAllPagesResult<T>>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One navigation entry a plugin contributes through its manifest's `ui` field. Structurally identical to
|
|
3
|
+
* `@rapidmx/restapi`'s `PluginUiNavItem`, defined locally so this package doesn't depend on restapi.
|
|
4
|
+
*/
|
|
5
|
+
export interface PluginUiNavItem {
|
|
6
|
+
/** Unique within its list. A plugin page passes this as its shell's `active` to highlight the entry. */
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
/** A same-origin path under the host's prefix (`/settings/...`, `/admin/...`, `/...`). */
|
|
10
|
+
href: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The navigation every enabled plugin with a successfully built UI contributes, supplied by the server's
|
|
14
|
+
* `WwwRoute`/`AdminConsoleRoute` `fetchProps` as the `pluginNav` page prop.
|
|
15
|
+
*/
|
|
16
|
+
export interface PluginNav {
|
|
17
|
+
/** Appended to `SettingsShell`'s section list. */
|
|
18
|
+
settingsSections?: PluginUiNavItem[];
|
|
19
|
+
/** Appended to `AdminShell`'s icon rail and mobile tab bar. */
|
|
20
|
+
adminNav?: PluginUiNavItem[];
|
|
21
|
+
/** Appended to `AppShell`'s app rail and mobile tab bar. */
|
|
22
|
+
appRail?: PluginUiNavItem[];
|
|
23
|
+
}
|
|
24
|
+
/** The page prop every www and admin page receives - extended by `AppShellProps` and `AdminShellProps`, so
|
|
25
|
+
* a page that spreads its props onto its shell passes `pluginNav` through with no code of its own. */
|
|
26
|
+
export interface PluginNavProps {
|
|
27
|
+
pluginNav?: PluginNav;
|
|
28
|
+
}
|
|
29
|
+
/** A same-origin absolute path: starts with `/`, but not `//` or `/\` (both of which browsers treat as
|
|
30
|
+
* protocol-relative URLs to another host). */
|
|
31
|
+
export declare function isSafePluginHref(href: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Appends a plugin's nav items after the core ones. Skips any item that isn't well-formed, whose `href`
|
|
34
|
+
* isn't a same-origin path (see `isSafePluginHref`), or whose id is already taken - by a core item or
|
|
35
|
+
* `reservedIds` (core ids always win) or by an earlier plugin item.
|
|
36
|
+
*/
|
|
37
|
+
export declare function mergePluginNavItems<T extends {
|
|
38
|
+
id: string;
|
|
39
|
+
}>(coreItems: T[], pluginItems: PluginUiNavItem[] | undefined, toItem: (item: PluginUiNavItem) => T, reservedIds?: string[]): T[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/** A same-origin absolute path: starts with `/`, but not `//` or `/\` (both of which browsers treat as
|
|
6
|
+
* protocol-relative URLs to another host). */
|
|
7
|
+
export function isSafePluginHref(href) {
|
|
8
|
+
return /^\/(?![/\\])/.test(href);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Appends a plugin's nav items after the core ones. Skips any item that isn't well-formed, whose `href`
|
|
12
|
+
* isn't a same-origin path (see `isSafePluginHref`), or whose id is already taken - by a core item or
|
|
13
|
+
* `reservedIds` (core ids always win) or by an earlier plugin item.
|
|
14
|
+
*/
|
|
15
|
+
export function mergePluginNavItems(coreItems, pluginItems, toItem, reservedIds = []) {
|
|
16
|
+
if (!pluginItems?.length) {
|
|
17
|
+
return coreItems;
|
|
18
|
+
}
|
|
19
|
+
const taken = new Set([...coreItems.map((item) => item.id), ...reservedIds]);
|
|
20
|
+
const merged = [...coreItems];
|
|
21
|
+
for (const item of pluginItems) {
|
|
22
|
+
if (typeof item?.id !== "string" ||
|
|
23
|
+
typeof item.label !== "string" ||
|
|
24
|
+
typeof item.href !== "string" ||
|
|
25
|
+
!isSafePluginHref(item.href) ||
|
|
26
|
+
taken.has(item.id)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
taken.add(item.id);
|
|
30
|
+
merged.push(toItem(item));
|
|
31
|
+
}
|
|
32
|
+
return merged;
|
|
33
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PublicKey } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
|
|
2
|
+
import type { Folder } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
3
|
+
export declare const POLL_INTERVAL_MS = 5000;
|
|
4
|
+
export interface LocalIndexLifecycleProps {
|
|
5
|
+
mailboxUid?: string;
|
|
6
|
+
mailboxKeys?: PublicKey[];
|
|
7
|
+
folders: Folder[];
|
|
8
|
+
/** Every mailbox the signed-in user can currently access. Once known, indexes on this device for any
|
|
9
|
+
* other mailbox (e.g. left behind by a different user who never signed out) are removed. */
|
|
10
|
+
accessibleMailboxUids?: string[];
|
|
11
|
+
}
|
|
12
|
+
/** Renders nothing - pure side-effect component, mounted by `MailShell.tsx`. */
|
|
13
|
+
export default function LocalIndexLifecycle({ mailboxUid, folders, accessibleMailboxUids }: LocalIndexLifecycleProps): null;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The actual AES-256-GCM block cipher `localIndexVFS.ts`'s `EncryptingVFS` uses to encrypt/decrypt one
|
|
3
|
+
* physical storage block - deliberately split out from that file so it can be exercised with real
|
|
4
|
+
* WebCrypto in a plain unit test, with no OPFS/Worker/wa-sqlite involved at all (none of those are
|
|
5
|
+
* available under Vitest's jsdom environment, matching the same "node" test environment convention
|
|
6
|
+
* `react-shared`'s other crypto modules already use for exactly this reason).
|
|
7
|
+
*
|
|
8
|
+
* See `localIndexVFS.ts`'s own doc comment for the full design rationale (page layout, why a fresh
|
|
9
|
+
* random nonce per write, why AAD binds to filename+block index). This module is pure mechanism; that
|
|
10
|
+
* one is where the design is explained.
|
|
11
|
+
*/
|
|
12
|
+
/** The logical SQLite page size `EncryptingVFS` is designed for - MUST match the `PRAGMA page_size` the
|
|
13
|
+
* database was created with (`localIndexWorker.ts` sets this before the first write). */
|
|
14
|
+
export declare const LOGICAL_BLOCK_SIZE = 4096;
|
|
15
|
+
/** One physical block on disk: `nonce || ciphertext || tag`. */
|
|
16
|
+
export declare const PHYSICAL_BLOCK_SIZE: number;
|
|
17
|
+
/** Thrown when a stored block fails AES-GCM authentication - a wrong/rotated key, on-disk corruption, or
|
|
18
|
+
* a block moved to the wrong position (see `localIndexVFS.ts`'s doc comment on why AAD binds position).
|
|
19
|
+
* The worker's caller treats this identically to a schema-version mismatch: discard the whole index and
|
|
20
|
+
* rebuild (spec §11 "Invalidation"). */
|
|
21
|
+
export declare class PageCorruptedError extends Error {
|
|
22
|
+
/** The underlying `DOMException`/error `crypto.subtle.decrypt()` threw, or an inner-VFS read failure
|
|
23
|
+
* - kept as a plain field rather than the ES2022 `Error` constructor's `cause` option, since
|
|
24
|
+
* web-client's `tsconfig.json` targets ES2020. */
|
|
25
|
+
readonly cause: unknown;
|
|
26
|
+
constructor(filename: string, blockIndex: number, cause: unknown);
|
|
27
|
+
}
|
|
28
|
+
export declare function importAesGcmKey(rawKey: Uint8Array): Promise<CryptoKey>;
|
|
29
|
+
/** Encrypts one logical block (`plaintext`, exactly `LOGICAL_BLOCK_SIZE` bytes) into its physical,
|
|
30
|
+
* on-disk representation (`PHYSICAL_BLOCK_SIZE` bytes: a fresh random nonce followed by
|
|
31
|
+
* ciphertext+tag). */
|
|
32
|
+
export declare function encryptBlock(key: CryptoKey, filename: string, blockIndex: number, plaintext: Uint8Array): Promise<Uint8Array>;
|
|
33
|
+
/** Inverse of `encryptBlock()`. Throws `PageCorruptedError` (never a raw `DOMException`) on auth
|
|
34
|
+
* failure - a wrong key, tampered ciphertext, or a block swapped into the wrong position (caught by the
|
|
35
|
+
* position-bound AAD not matching). */
|
|
36
|
+
export declare function decryptBlock(key: CryptoKey, filename: string, blockIndex: number, physical: Uint8Array): Promise<Uint8Array>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Tier 2 local index's background builder (`specs/search.md` §11 "Initial build... incrementally,
|
|
3
|
+
* newest-first"). Runs on the main thread - the actual storage I/O and encryption happen in
|
|
4
|
+
* `localIndexWorker.ts` (off the UI thread, per that file's own doc comment); this module's own work is
|
|
5
|
+
* orchestration (deciding what to fetch) plus already-async fetch/decrypt calls, not CPU-heavy work that
|
|
6
|
+
* would itself need to move off-thread.
|
|
7
|
+
*
|
|
8
|
+
* **Known scoping simplification**: walks the mailbox's real mail folders one at a time (a fixed,
|
|
9
|
+
* reasonable priority order - Inbox and Sent first), each already newest-first via `listMessages()`'s own
|
|
10
|
+
* default sort, rather than a true interleaved k-way merge producing one single globally-newest-first
|
|
11
|
+
* stream across folders. A message in, say, Sent slightly older than the *oldest-processed-so-far*
|
|
12
|
+
* message in Inbox can therefore be indexed slightly out of true global date order relative to it. Given
|
|
13
|
+
* the byte-budget eviction below is itself date-based (oldest `date_for_sort` first, enforced by
|
|
14
|
+
* `localIndexWorker.ts`'s own `applyEviction()` after every insert - see that file), a small amount of
|
|
15
|
+
* cross-folder interleaving imprecision here does not affect *what* ultimately survives the window, only
|
|
16
|
+
* the exact order entities are inserted (and therefore briefly evicted-and-reinserted) in.
|
|
17
|
+
*
|
|
18
|
+
* Only encrypted messages (`subject === "[...]"`) are indexed - see `ENCRYPTED_SUBJECT_PLACEHOLDER`'s
|
|
19
|
+
* own precedent in `apps/www/index.tsx`'s inbox-list decrypt work. An unencrypted message is already
|
|
20
|
+
* fully searchable via Tier 1; indexing it here too would spend this index's bounded byte budget on
|
|
21
|
+
* content that didn't need it.
|
|
22
|
+
*
|
|
23
|
+
* **Verification seals.** A message this pass decrypts that has no seal for the vault's current `masterKeyGeneration` is
|
|
24
|
+
* evaluated with `evaluateMessageSecurityWithSeal()`, against the sender's pinned signing keys from the reader's
|
|
25
|
+
* contacts (`pinnedSigners.ts`, the message pane's own source, looked up once per sender per pass), so a verified
|
|
26
|
+
* message gets its seal without being opened (see `apps/shared/components/mail/verificationSeals.ts`). Pins only change
|
|
27
|
+
* the security state, never the recovered subject or body this index stores. Seal writes are best effort and bounded:
|
|
28
|
+
* at most `SEAL_WRITE_CONCURRENCY` in flight, at most `MAX_SEAL_WRITES_PER_PASS` per pass (past it, messages are
|
|
29
|
+
* evaluated without seals), failures ignored, and nothing new starts once the pass is aborted or its keys are destroyed.
|
|
30
|
+
* A seal carries only a hash, fingerprint, state and time, never plaintext. With no readable vault generation the pass
|
|
31
|
+
* seals nothing. Signed-only (unencrypted) messages aren't sealed here: this pass never fetches their raw MIME (see
|
|
32
|
+
* above), and fetching it only to seal would cost a download per message; they are sealed when opened.
|
|
33
|
+
*/
|
|
34
|
+
import { type Folder } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
35
|
+
import type { UnlockedKeys } from "@rapidmx/react-shared/crypto/keySession.js";
|
|
36
|
+
/** §11's Window Sizing table's time-floor column - explicitly unvalidated starting-point default per the
|
|
37
|
+
* spec's own §16 "Measurement Task" ("cannot be supplied by design work... MUST NOT be treated as
|
|
38
|
+
* validated"). Used as-is rather than invented/adjusted here. Not user-adjustable today (unlike the byte
|
|
39
|
+
* budget - see `localIndexSizePreference.ts`) - nothing in this codebase has asked for that yet. */
|
|
40
|
+
export declare const WEB_TIME_FLOOR_MONTHS = 12;
|
|
41
|
+
/** Either bound as `0` means "no limit": `applyEviction()` (`localIndexWorker.ts`) already treats a
|
|
42
|
+
* falsy byte budget as unconfigured/unenforced, and `buildLocalIndex()` below mirrors that same
|
|
43
|
+
* convention for `timeFloorMonths` so a single `0` means the same thing in both dimensions. */
|
|
44
|
+
export interface LocalIndexWindowConfig {
|
|
45
|
+
timeFloorMonths: number;
|
|
46
|
+
byteBudgetBytes: number;
|
|
47
|
+
}
|
|
48
|
+
/** How many times a pass walks a folder whose total count changed during the walk. `listMessages()` only
|
|
49
|
+
* pages by offset, so a message removed from an already-walked page shifts a later one onto it unseen;
|
|
50
|
+
* only a folder whose count held steady across a whole walk is trusted for completeness and pruning. */
|
|
51
|
+
export declare const MAX_WALK_ATTEMPTS = 3;
|
|
52
|
+
/** Subtracted from the pass's start time before it's recorded as the end of guaranteed coverage - a
|
|
53
|
+
* message's server-assigned `receivedDate` can be ahead of this device's clock. */
|
|
54
|
+
export declare const CLOCK_SKEW_MARGIN_MS: number;
|
|
55
|
+
/** How many raw-MIME fetch+decrypt calls run at once - a page of 100 encrypted messages used to fire all
|
|
56
|
+
* 100 requests simultaneously. */
|
|
57
|
+
export declare const FETCH_CONCURRENCY = 6;
|
|
58
|
+
/** How many verification seal writes run at once during a pass. */
|
|
59
|
+
export declare const SEAL_WRITE_CONCURRENCY = 2;
|
|
60
|
+
/** The most verification seals one pass writes. */
|
|
61
|
+
export declare const MAX_SEAL_WRITES_PER_PASS = 200;
|
|
62
|
+
/** Runs one full incremental build pass for `mailboxUid`: sets the window, walks mail folders newest-first
|
|
63
|
+
* (per this module's own doc comment on the folder-order simplification), decrypts and indexes only
|
|
64
|
+
* `"[...]"`-subject messages until each folder's own coverage passes the time floor, then clears the
|
|
65
|
+
* `building` flag. Messages already indexed at the same version are skipped, fetches run at most
|
|
66
|
+
* `FETCH_CONCURRENCY` at a time, and messages older than the eviction watermark (`WindowState.evictedBefore`)
|
|
67
|
+
* are neither fetched nor walked past. A failure partway through leaves whatever was indexed so far in place
|
|
68
|
+
* and records the pass as incomplete (spec §11's "incomplete-index UX... MUST indicate that coverage is
|
|
69
|
+
* partial" - see `Coverage.complete`). Rows the server no longer lists (deleted or moved elsewhere by any
|
|
70
|
+
* client) are pruned only within folders whose count held steady for a whole walk (see `MAX_WALK_ATTEMPTS`).
|
|
71
|
+
*
|
|
72
|
+
* Only one pass runs per mailbox (a newer call cancels the older), and every Worker call carries this pass's
|
|
73
|
+
* generation, so a pass outlived by a destroy can't write anything (see `cancelLocalIndexBuild()`).
|
|
74
|
+
*
|
|
75
|
+
* `windowConfig` defaults to this device's own configured byte budget (`getLocalIndexByteBudget()` -
|
|
76
|
+
* 500 MB in a browser tab, 1 GB in Electron, or whatever the user has since set in Settings > Encryption)
|
|
77
|
+
* alongside the fixed time floor above. Evaluated fresh on every call with no explicit override, so a
|
|
78
|
+
* preference change in Settings takes effect starting with this mailbox's next build pass (its next
|
|
79
|
+
* unlock), without requiring a reload.
|
|
80
|
+
*/
|
|
81
|
+
export declare function buildLocalIndex(mailboxUid: string, unlocked: UnlockedKeys, folders: Folder[], windowConfig?: LocalIndexWindowConfig): Promise<void>;
|
|
82
|
+
/** Stops a mailbox's running build pass, if any (e.g. its keys were just destroyed), resolving once it has
|
|
83
|
+
* wound down. The Worker independently rejects the pass's later calls once the index is destroyed (see
|
|
84
|
+
* `GenerationParams`); this just stops the fetching and decrypting too. Never rejects. */
|
|
85
|
+
export declare function cancelLocalIndexBuild(mailboxUid: string): Promise<void>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Derives this mailbox's local-index page-encryption key from its already-unlocked master key. Every
|
|
2
|
+
* call with the same `(masterKey, mailboxUid)` pair returns the identical key - callers never need to
|
|
3
|
+
* persist it themselves. */
|
|
4
|
+
export declare function deriveLocalIndexKey(masterKey: Uint8Array, mailboxUid: string): Promise<Uint8Array>;
|