@rapidmx/mapi-plugin 1.0.0-beta.3 → 1.0.0-beta.4
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 +4 -4
- package/dist/lib/BaseMapiEmsmdbRoute.js +211 -36
- package/dist/lib/BaseMapiEmsmdbRoute.js.map +1 -1
- package/dist/lib/MapiSessionManager.js +404 -48
- package/dist/lib/MapiSessionManager.js.map +1 -1
- package/dist/lib/RegexPatternUtils.js +30 -0
- package/dist/lib/RegexPatternUtils.js.map +1 -0
- package/dist/lib/RopDispatcher.js +130 -19
- package/dist/lib/RopDispatcher.js.map +1 -1
- package/dist/lib/codec/AppointmentRecurrence.js +22 -21
- package/dist/lib/codec/AppointmentRecurrence.js.map +1 -1
- package/dist/lib/codec/BufferCursor.js +41 -12
- package/dist/lib/codec/BufferCursor.js.map +1 -1
- package/dist/lib/codec/GlobalObjectId.js +26 -20
- package/dist/lib/codec/GlobalObjectId.js.map +1 -1
- package/dist/lib/codec/MapiTimeZone.js +43 -10
- package/dist/lib/codec/MapiTimeZone.js.map +1 -1
- package/dist/lib/codec/PropertyValue.js +28 -2
- package/dist/lib/codec/PropertyValue.js.map +1 -1
- package/dist/lib/codec/RopBuffer.js +15 -3
- package/dist/lib/codec/RopBuffer.js.map +1 -1
- package/dist/lib/mongo/MapiEmsmdbRouteMongo.js +2 -1
- package/dist/lib/mongo/MapiEmsmdbRouteMongo.js.map +1 -1
- package/dist/lib/nspi/NspiCodec.js +5 -5
- package/dist/lib/nspi/NspiCodec.js.map +1 -1
- package/dist/lib/nspi/NspiGetMatchesHandler.js +14 -14
- package/dist/lib/nspi/NspiGetMatchesHandler.js.map +1 -1
- package/dist/lib/rop/AddressList.js +109 -0
- package/dist/lib/rop/AddressList.js.map +1 -0
- package/dist/lib/rop/CalendarEventTarget.js +7 -4
- package/dist/lib/rop/CalendarEventTarget.js.map +1 -1
- package/dist/lib/rop/ContactTarget.js +2 -6
- package/dist/lib/rop/ContactTarget.js.map +1 -1
- package/dist/lib/rop/ContentsTable.js +60 -0
- package/dist/lib/rop/ContentsTable.js.map +1 -0
- package/dist/lib/rop/ExecuteBudget.js +82 -0
- package/dist/lib/rop/ExecuteBudget.js.map +1 -0
- package/dist/lib/rop/FastTransferStream.js +99 -2
- package/dist/lib/rop/FastTransferStream.js.map +1 -1
- package/dist/lib/rop/FolderTarget.js +13 -6
- package/dist/lib/rop/FolderTarget.js.map +1 -1
- package/dist/lib/rop/HandleDataCache.js +412 -0
- package/dist/lib/rop/HandleDataCache.js.map +1 -0
- package/dist/lib/rop/MeetingMessageClassHandler.js +194 -48
- package/dist/lib/rop/MeetingMessageClassHandler.js.map +1 -1
- package/dist/lib/rop/MessageBodyStream.js +33 -7
- package/dist/lib/rop/MessageBodyStream.js.map +1 -1
- package/dist/lib/rop/MessageTarget.js +17 -4
- package/dist/lib/rop/MessageTarget.js.map +1 -1
- package/dist/lib/rop/NamedPropertyRegistry.js +8 -5
- package/dist/lib/rop/NamedPropertyRegistry.js.map +1 -1
- package/dist/lib/rop/PropertyResolvers.js +3 -1
- package/dist/lib/rop/PropertyResolvers.js.map +1 -1
- package/dist/lib/rop/RepoPaging.js +54 -0
- package/dist/lib/rop/RepoPaging.js.map +1 -0
- package/dist/lib/rop/RestapiRules.js +52 -0
- package/dist/lib/rop/RestapiRules.js.map +1 -0
- package/dist/lib/rop/RopCreateMessageHandler.js +4 -2
- package/dist/lib/rop/RopCreateMessageHandler.js.map +1 -1
- package/dist/lib/rop/RopDeleteFolderHandler.js +112 -77
- package/dist/lib/rop/RopDeleteFolderHandler.js.map +1 -1
- package/dist/lib/rop/RopDeleteMessagesHandler.js +17 -1
- package/dist/lib/rop/RopDeleteMessagesHandler.js.map +1 -1
- package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js +14 -14
- package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js.map +1 -1
- package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js +13 -14
- package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js.map +1 -1
- package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js +33 -32
- package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js.map +1 -1
- package/dist/lib/rop/RopGetContentsTableHandler.js +17 -38
- package/dist/lib/rop/RopGetContentsTableHandler.js.map +1 -1
- package/dist/lib/rop/RopGetHierarchyTableHandler.js +4 -2
- package/dist/lib/rop/RopGetHierarchyTableHandler.js.map +1 -1
- package/dist/lib/rop/RopGetPropertiesSpecificHandler.js +6 -8
- package/dist/lib/rop/RopGetPropertiesSpecificHandler.js.map +1 -1
- package/dist/lib/rop/RopHandler.js +5 -1
- package/dist/lib/rop/RopHandler.js.map +1 -1
- package/dist/lib/rop/RopLogonHandler.js +5 -2
- package/dist/lib/rop/RopLogonHandler.js.map +1 -1
- package/dist/lib/rop/RopOpenFolderHandler.js +3 -1
- package/dist/lib/rop/RopOpenFolderHandler.js.map +1 -1
- package/dist/lib/rop/RopOpenMessageHandler.js +3 -1
- package/dist/lib/rop/RopOpenMessageHandler.js.map +1 -1
- package/dist/lib/rop/RopOpenStreamHandler.js +11 -8
- package/dist/lib/rop/RopOpenStreamHandler.js.map +1 -1
- package/dist/lib/rop/RopQueryRowsHandler.js +31 -5
- package/dist/lib/rop/RopQueryRowsHandler.js.map +1 -1
- package/dist/lib/rop/RopReadStreamHandler.js +22 -4
- package/dist/lib/rop/RopReadStreamHandler.js.map +1 -1
- package/dist/lib/rop/RopReleaseHandler.js +4 -1
- package/dist/lib/rop/RopReleaseHandler.js.map +1 -1
- package/dist/lib/rop/RopSaveChangesMessageHandler.js +66 -7
- package/dist/lib/rop/RopSaveChangesMessageHandler.js.map +1 -1
- package/dist/lib/rop/RopSetColumnsHandler.js +6 -8
- package/dist/lib/rop/RopSetColumnsHandler.js.map +1 -1
- package/dist/lib/rop/RopSetPropertiesHandler.js +23 -12
- package/dist/lib/rop/RopSetPropertiesHandler.js.map +1 -1
- package/dist/lib/rop/RopSubmitMessageHandler.js +144 -120
- package/dist/lib/rop/RopSubmitMessageHandler.js.map +1 -1
- package/dist/lib/rop/RopWriteStreamHandler.js +37 -10
- package/dist/lib/rop/RopWriteStreamHandler.js.map +1 -1
- package/dist/lib/rop/TaskTarget.js +2 -6
- package/dist/lib/rop/TaskTarget.js.map +1 -1
- package/dist/lib/rop/TransportSend.js +16 -0
- package/dist/lib/rop/TransportSend.js.map +1 -0
- package/dist/lib/sql/MapiEmsmdbRouteSQL.js +2 -1
- package/dist/lib/sql/MapiEmsmdbRouteSQL.js.map +1 -1
- package/dist/types/BaseMapiEmsmdbRoute.d.ts +52 -2
- package/dist/types/MapiSessionManager.d.ts +218 -63
- package/dist/types/RegexPatternUtils.d.ts +14 -0
- package/dist/types/RopDispatcher.d.ts +47 -11
- package/dist/types/codec/BufferCursor.d.ts +14 -0
- package/dist/types/codec/GlobalObjectId.d.ts +9 -5
- package/dist/types/codec/PropertyValue.d.ts +10 -0
- package/dist/types/codec/RopBuffer.d.ts +7 -0
- package/dist/types/mongo/MapiEmsmdbRouteMongo.d.ts +1 -0
- package/dist/types/nspi/NspiGetMatchesHandler.d.ts +2 -0
- package/dist/types/rop/AddressList.d.ts +48 -0
- package/dist/types/rop/CalendarEventTarget.d.ts +7 -3
- package/dist/types/rop/ContactTarget.d.ts +3 -3
- package/dist/types/rop/ContentsTable.d.ts +16 -0
- package/dist/types/rop/ExecuteBudget.d.ts +48 -0
- package/dist/types/rop/FastTransferStream.d.ts +41 -2
- package/dist/types/rop/FolderTarget.d.ts +3 -2
- package/dist/types/rop/HandleDataCache.d.ts +145 -0
- package/dist/types/rop/MeetingMessageClassHandler.d.ts +23 -29
- package/dist/types/rop/MessageBodyStream.d.ts +16 -7
- package/dist/types/rop/MessageTarget.d.ts +10 -3
- package/dist/types/rop/NamedPropertyRegistry.d.ts +6 -2
- package/dist/types/rop/PropertyResolvers.d.ts +1 -1
- package/dist/types/rop/RepoPaging.d.ts +30 -0
- package/dist/types/rop/RestapiRules.d.ts +32 -0
- package/dist/types/rop/RopCreateMessageHandler.d.ts +1 -0
- package/dist/types/rop/RopDeleteFolderHandler.d.ts +36 -21
- package/dist/types/rop/RopDeleteMessagesHandler.d.ts +7 -0
- package/dist/types/rop/RopFastTransferSourceCopyPropertiesHandler.d.ts +1 -0
- package/dist/types/rop/RopFastTransferSourceCopyToHandler.d.ts +1 -0
- package/dist/types/rop/RopFastTransferSourceGetBufferHandler.d.ts +10 -11
- package/dist/types/rop/RopGetContentsTableHandler.d.ts +7 -13
- package/dist/types/rop/RopGetHierarchyTableHandler.d.ts +1 -0
- package/dist/types/rop/RopHandler.d.ts +25 -1
- package/dist/types/rop/RopOpenFolderHandler.d.ts +1 -0
- package/dist/types/rop/RopOpenMessageHandler.d.ts +1 -0
- package/dist/types/rop/RopOpenStreamHandler.d.ts +1 -0
- package/dist/types/rop/RopQueryRowsHandler.d.ts +3 -0
- package/dist/types/rop/RopReadStreamHandler.d.ts +3 -1
- package/dist/types/rop/RopReleaseHandler.d.ts +1 -0
- package/dist/types/rop/RopSaveChangesMessageHandler.d.ts +16 -1
- package/dist/types/rop/RopSubmitMessageHandler.d.ts +33 -23
- package/dist/types/rop/RopWriteStreamHandler.d.ts +18 -8
- package/dist/types/rop/TaskTarget.d.ts +3 -3
- package/dist/types/rop/TransportSend.d.ts +10 -0
- package/dist/types/sql/MapiEmsmdbRouteSQL.d.ts +1 -0
- package/package.json +3 -3
|
@@ -21,5 +21,12 @@ export interface RopBuffer {
|
|
|
21
21
|
* ROPs within `ropsList` reference them by index. */
|
|
22
22
|
handleTable: number[];
|
|
23
23
|
}
|
|
24
|
+
/** The largest `ServerObjectHandleTable` accepted. Every ROP addresses a handle by a one-byte index, so a table
|
|
25
|
+
* with more than 255 entries can't be referenced anyway; a larger one is only a way to make the server allocate
|
|
26
|
+
* and echo back an oversized array. */
|
|
27
|
+
export declare const MAX_HANDLE_TABLE_ENTRIES = 255;
|
|
24
28
|
export declare function encodeRopBuffer(buf: RopBuffer): Buffer;
|
|
29
|
+
/** Throws a `RangeError` for a malformed buffer: a `RopSize` smaller than its own field or larger than the
|
|
30
|
+
* buffer, a handle table that isn't a whole number of 4-byte entries, or one with more than
|
|
31
|
+
* `MAX_HANDLE_TABLE_ENTRIES` entries. */
|
|
25
32
|
export declare function decodeRopBuffer(buffer: Buffer): RopBuffer;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { HttpRequest, HttpResponse, RepoUtils } from "@rapidrest/service-core";
|
|
2
|
+
/** The most rows one `GetMatches` lookup asks the repo for per query - `RepoUtils.find()`'s own page cap. */
|
|
3
|
+
export declare const MAX_MATCH_ROWS = 1000;
|
|
2
4
|
/**
|
|
3
5
|
* `GetMatches` request type (`[MS-OXCMAPIHTTP]` §2.2.5.5, backed by `[MS-OXNSPI]`'s own `NspiGetMatches`
|
|
4
6
|
* method): searches the GAL (this library's `Contact` records, the same source of truth EAS's own `Search`
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { RopContext } from "./RopHandler.js";
|
|
2
|
+
/** Splits a `PidTagDisplayTo`/`Cc`/`Bcc`-style string on the semicolons Outlook separates recipients with,
|
|
3
|
+
* trimming and dropping empty entries. Commas are not separators: a resolved recipient's display name commonly
|
|
4
|
+
* contains one (`Doe, Jane <jane@example.com>`). */
|
|
5
|
+
export declare function splitAddressList(value: string | undefined): string[];
|
|
6
|
+
/** `true` for a plain SMTP address (see `PLAIN_EMAIL_ADDRESS`) no longer than RFC 5321's 254 characters. These
|
|
7
|
+
* strings reach MIME headers, the SMTP envelope and iCalendar `mailto:` lines, where a CR/LF would inject content. */
|
|
8
|
+
export declare function isPlainEmailAddress(value: string): boolean;
|
|
9
|
+
/** One entry of a display string: an address with an optional display name, a display name alone (`address`
|
|
10
|
+
* undefined, still to be resolved), or something that can't be used (`invalid`). */
|
|
11
|
+
export interface RecipientEntry {
|
|
12
|
+
name: string;
|
|
13
|
+
address?: string;
|
|
14
|
+
invalid?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parses one `;`-separated entry with nodemailer's `addressparser`: `jane@example.com`, `Jane <jane@example.com>`,
|
|
18
|
+
* `"Doe, Jane" <jane@example.com>` or a bare display name `Jane Doe`. An entry holding several addresses yields one
|
|
19
|
+
* recipient each. Any address that isn't a plain SMTP address marks the entry invalid.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseRecipientEntry(entry: string): RecipientEntry[];
|
|
22
|
+
/** Every entry of a `PidTagDisplayTo`/`Cc`/`Bcc`-style string - see `parseRecipientEntry`. */
|
|
23
|
+
export declare function parseRecipientList(value: string | undefined): RecipientEntry[];
|
|
24
|
+
/** The plain addresses in a `PidTagDisplayTo`/`Cc`/`Bcc`-style string, including those written as
|
|
25
|
+
* `Name <address>`, skipping display names without an address and anything invalid. */
|
|
26
|
+
export declare function parseAddressList(value: string | undefined): string[];
|
|
27
|
+
/** A recipient ready to send to. */
|
|
28
|
+
export interface ResolvedRecipient {
|
|
29
|
+
name: string;
|
|
30
|
+
address: string;
|
|
31
|
+
}
|
|
32
|
+
/** The outcome of `resolveRecipientList`: the usable recipients, and the entries that were display names with no
|
|
33
|
+
* single matching contact (`unresolved`) or not usable at all (`invalid`). */
|
|
34
|
+
export interface RecipientResolution {
|
|
35
|
+
recipients: ResolvedRecipient[];
|
|
36
|
+
unresolved: string[];
|
|
37
|
+
invalid: string[];
|
|
38
|
+
}
|
|
39
|
+
/** The most recipients (To, Cc and Bcc together) one submitted message or meeting may have - the same limit
|
|
40
|
+
* `@rapidmx/activesync`'s `ComposeMailCommand` enforces (`MAX_COMPOSE_RECIPIENTS`). */
|
|
41
|
+
export declare const MAX_RECIPIENTS_PER_MESSAGE = 500;
|
|
42
|
+
/**
|
|
43
|
+
* Resolves a display string into addresses. An entry carrying an address is used as is; a bare display name (Outlook
|
|
44
|
+
* writes names, not addresses, into `PidTagDisplayTo` once it has resolved a recipient against an address book) is
|
|
45
|
+
* looked up among the caller's own contacts by exact display name and used when exactly one contact matches, with
|
|
46
|
+
* that contact's first valid email address.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveRecipientList(value: string | undefined, context: Pick<RopContext, "mailboxUid" | "contactRepo">): Promise<RecipientResolution>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { RepoUtils } from "@rapidrest/service-core";
|
|
2
2
|
import { Attendee, BusyStatus, RecurrenceRule } from "@rapidmx/restapi";
|
|
3
|
+
import type { ExecuteBudget } from "./ExecuteBudget.js";
|
|
4
|
+
import { type RepoSort } from "./RepoPaging.js";
|
|
3
5
|
/**
|
|
4
6
|
* The `RopGetContentsTable`/`RopOpenMessage` analog of `MessageTarget.ts`, for a `Folder` of type `CALENDAR`:
|
|
5
7
|
* resolves a `"calendarEvent:<uid>"` row/handle target into the display data a calendar-item row or an opened
|
|
@@ -24,6 +26,8 @@ export interface CalendarEventTargetInfo {
|
|
|
24
26
|
* vanished (soft-deleted or otherwise) - the same "don't fail the whole ROP over one stale row" principle
|
|
25
27
|
* `MessageTarget.resolveMessageInfo`/`FolderTarget.resolveFolderInfo` already apply. */
|
|
26
28
|
export declare function resolveCalendarEventInfo(target: string, calendarEventRepo: RepoUtils<any>): Promise<CalendarEventTargetInfo>;
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
/** The order a calendar folder's events are listed in: latest start first, ties broken by `uid`. */
|
|
30
|
+
export declare const CALENDAR_EVENT_SORT: RepoSort;
|
|
31
|
+
/** Resolves the calendar events directly in `folderUid`, latest first and capped at `MAX_COLLECTION_ROWS`, as
|
|
32
|
+
* `"calendarEvent:<uid>"` target strings. */
|
|
33
|
+
export declare function resolveFolderCalendarEvents(folderUid: string, calendarEventRepo: RepoUtils<any>, budget?: ExecuteBudget): Promise<string[]>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RepoUtils } from "@rapidrest/service-core";
|
|
2
|
+
import type { RepoSort } from "./RepoPaging.js";
|
|
2
3
|
/**
|
|
3
4
|
* The `RopGetContentsTable`/`RopOpenMessage` analog of `MessageTarget.ts`, for a `Folder` of type `CONTACTS`:
|
|
4
5
|
* resolves a `"contact:<uid>"` row/handle target into the display data a contact-item row or an opened
|
|
@@ -24,6 +25,5 @@ export interface ContactTargetInfo {
|
|
|
24
25
|
* (soft-deleted or otherwise) - the same "don't fail the whole ROP over one stale row" principle
|
|
25
26
|
* `MessageTarget.resolveMessageInfo`/`CalendarEventTarget.resolveCalendarEventInfo` already apply. */
|
|
26
27
|
export declare function resolveContactInfo(target: string, contactRepo: RepoUtils<any>): Promise<ContactTargetInfo>;
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
export declare function resolveFolderContacts(folderUid: string, contactRepo: RepoUtils<any>): Promise<string[]>;
|
|
28
|
+
/** The order a `CONTACTS` folder's contents table lists contacts in (see `ContentsTable.ts`). */
|
|
29
|
+
export declare const CONTACT_SORT: RepoSort;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { MapiObjectHandle } from "../MapiSessionManager.js";
|
|
2
|
+
import type { RopContext } from "./RopHandler.js";
|
|
3
|
+
export type ContentsKind = NonNullable<MapiObjectHandle["contentsKind"]>;
|
|
4
|
+
/** Which repo a folder's items live in, decided by the folder's own `type`: a `Contact`/`Task`/`CalendarEvent` is
|
|
5
|
+
* its own entity, never a `Message` row. */
|
|
6
|
+
export declare function resolveContentsKind(folderUid: string, context: RopContext): Promise<ContentsKind>;
|
|
7
|
+
/**
|
|
8
|
+
* The `"<kind>:<uid>"` row targets at positions `[start, start + count)` of a contents table (a `"folder:<uid>"`
|
|
9
|
+
* table with `contentsKind` set), read straight from the database in a fixed, stable order. A context without the
|
|
10
|
+
* matching optional repo yields no rows.
|
|
11
|
+
*
|
|
12
|
+
* Reading a window per `RopQueryRows` instead of snapshotting every row at `RopGetContentsTable` time keeps session
|
|
13
|
+
* state small and makes no folder too large to page through. The trade-off is that a message added or removed
|
|
14
|
+
* between two `RopQueryRows` calls can shift the rows that follow.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveContentsWindow(context: RopContext, table: MapiObjectHandle, start: number, count: number): Promise<string[]>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** The most rows (items or folders) whose properties one `Execute` resolves, across all of its ROPs. */
|
|
2
|
+
export declare const MAX_ROWS_RESOLVED_PER_EXECUTE = 20000;
|
|
3
|
+
/** The most bytes one `Execute` builds or parses (FastTransfer streams and message bodies), across all of its ROPs. */
|
|
4
|
+
export declare const MAX_BYTES_BUILT_PER_EXECUTE: number;
|
|
5
|
+
/** The most database queries (paged reads, table lookups, deletes) one `Execute` runs through the budgeted paths. */
|
|
6
|
+
export declare const MAX_QUERIES_PER_EXECUTE = 20000;
|
|
7
|
+
/** The most rows one `Execute` reads back from paged queries (folder lists, folder walks, item lists). */
|
|
8
|
+
export declare const MAX_ROWS_FETCHED_PER_EXECUTE = 100000;
|
|
9
|
+
/** The most `RopSubmitMessage`s one `Execute` runs. Each one can relay mail, so a request can't repeat it 1024 times. */
|
|
10
|
+
export declare const MAX_SUBMITS_PER_EXECUTE = 16;
|
|
11
|
+
/** Thrown when an `Execute` has used up its `ExecuteBudget`. The ROP that hit it fails; see `RopDispatcher`. */
|
|
12
|
+
export declare class WorkBudgetExceededError extends Error {
|
|
13
|
+
constructor(what: string);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The work one `Execute` may do. Limits on ROP count and per-ROP sizes alone still let one request chain many
|
|
17
|
+
* expensive ROPs (a dozen 10000-row FastTransfer dumps, the same large body parsed by several streams, or a thousand
|
|
18
|
+
* hierarchy tables or folder walks), so rows resolved, bytes built, queries run, rows fetched and submits are counted
|
|
19
|
+
* across the whole request. Every charge happens before the work it pays for. Also dedupes message body parses within
|
|
20
|
+
* the request (`bodies`).
|
|
21
|
+
*/
|
|
22
|
+
export declare class ExecuteBudget {
|
|
23
|
+
rowsRemaining: number;
|
|
24
|
+
bytesRemaining: number;
|
|
25
|
+
queriesRemaining: number;
|
|
26
|
+
fetchedRowsRemaining: number;
|
|
27
|
+
submitsRemaining: number;
|
|
28
|
+
/** Message uid -> its decoded body, so opening the same body twice in one request parses it once. */
|
|
29
|
+
readonly bodies: Map<string, Promise<Buffer<ArrayBufferLike>>>;
|
|
30
|
+
constructor(maxRows?: number, maxBytes?: number, limits?: {
|
|
31
|
+
maxQueries?: number;
|
|
32
|
+
maxFetchedRows?: number;
|
|
33
|
+
maxSubmits?: number;
|
|
34
|
+
});
|
|
35
|
+
/** Uses `count` rows of the budget, throwing `WorkBudgetExceededError` once it is exhausted. */
|
|
36
|
+
chargeRows(count?: number): void;
|
|
37
|
+
/** Uses `count` bytes of the budget, throwing `WorkBudgetExceededError` once it is exhausted. */
|
|
38
|
+
chargeBytes(count: number): void;
|
|
39
|
+
/** Throws `WorkBudgetExceededError` when no bytes are left, so work that can't be sized up front (fetching a blob)
|
|
40
|
+
* isn't started at all. */
|
|
41
|
+
assertBytesLeft(): void;
|
|
42
|
+
/** Uses `count` queries of the budget. Called before running them. */
|
|
43
|
+
chargeQueries(count?: number): void;
|
|
44
|
+
/** Uses `count` fetched rows of the budget. */
|
|
45
|
+
chargeFetchedRows(count: number): void;
|
|
46
|
+
/** Uses one submit of the budget. */
|
|
47
|
+
chargeSubmit(): void;
|
|
48
|
+
}
|
|
@@ -1,18 +1,57 @@
|
|
|
1
1
|
import { PropertyType } from "../codec/PropertyValue.js";
|
|
2
|
-
import type
|
|
3
|
-
import type
|
|
2
|
+
import { type MapiObjectHandle } from "../MapiSessionManager.js";
|
|
3
|
+
import { type RopContext } from "./RopHandler.js";
|
|
4
4
|
interface PropertyColumn {
|
|
5
5
|
propertyId: number;
|
|
6
6
|
propertyType: PropertyType;
|
|
7
7
|
}
|
|
8
|
+
/** The largest FastTransfer stream built for one handle; a larger source fails with `MAPI_E_TOO_BIG`. */
|
|
9
|
+
export declare const MAX_FAST_TRANSFER_BYTES: number;
|
|
10
|
+
/** How long a built stream is kept for paging out. A client pages a stream out right after opening it; one that
|
|
11
|
+
* leaves it longer than this has to open the transfer again. */
|
|
12
|
+
export declare const FAST_TRANSFER_TTL_SECONDS: number;
|
|
13
|
+
/** Thrown while building a stream that has grown past its byte limit. */
|
|
14
|
+
export declare class FastTransferTooBigError extends Error {
|
|
15
|
+
constructor();
|
|
16
|
+
}
|
|
8
17
|
/**
|
|
9
18
|
* Builds the complete FastTransfer stream for `handle` (a `"folder"` or `"message"` Server object). `columns`,
|
|
10
19
|
* when given (a `RopFastTransferSourceCopyProperties` explicit include list), replaces every default column set
|
|
11
20
|
* uniformly at both folder- and message-level; `excludePropertyIds` (a `RopFastTransferSourceCopyTo` exclude
|
|
12
21
|
* list) is only ever applied to this pragmatic subset's own default columns, per the same reasoning.
|
|
22
|
+
*
|
|
23
|
+
* The size is checked after every item, so a huge folder stops being built as soon as it passes `maxBytes`
|
|
24
|
+
* (`FastTransferTooBigError`) instead of being fully built first. Every item resolved and the finished stream's bytes
|
|
25
|
+
* count against the request's `ExecuteBudget` when there is one.
|
|
13
26
|
*/
|
|
14
27
|
export declare function buildFastTransferStream(handle: MapiObjectHandle, context: RopContext, options?: {
|
|
15
28
|
columns?: PropertyColumn[];
|
|
16
29
|
excludePropertyIds?: ReadonlySet<number>;
|
|
30
|
+
maxBytes?: number;
|
|
17
31
|
}): Promise<Buffer>;
|
|
32
|
+
/** Why `loadFastTransferBuffer` has no stream: `"tooBig"` when a rebuild passed `MAX_FAST_TRANSFER_BYTES`, `"lost"`
|
|
33
|
+
* when the stored stream is gone part-way through paging. */
|
|
34
|
+
export type FastTransferLoadFailure = "tooBig" | "lost";
|
|
35
|
+
/**
|
|
36
|
+
* The built stream for the `"fastTransfer"` handle at `handleIndex`, from the shared `HandleDataStore` (Redis when
|
|
37
|
+
* configured, so any replica can page it out).
|
|
38
|
+
*
|
|
39
|
+
* When the stream is gone (expired, evicted, or built by a replica without shared storage), it is rebuilt only if
|
|
40
|
+
* nothing has been paged out yet. Part-way through, a rebuild could differ from what the client already has (items
|
|
41
|
+
* added or changed since), and continuing at the old offset would hand it bytes from a different stream, so that
|
|
42
|
+
* case reports `"lost"` and the client restarts the transfer. A rebuild is held to `MAX_FAST_TRANSFER_BYTES` like
|
|
43
|
+
* the original build, and storing it to the session's and user's handle data quota (`handleDataOwners`); either
|
|
44
|
+
* failing reports `"tooBig"`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function loadFastTransferBuffer(context: RopContext, handleIndex: number, transfer: MapiObjectHandle): Promise<Buffer | FastTransferLoadFailure>;
|
|
47
|
+
/**
|
|
48
|
+
* Shared by `RopFastTransferSourceCopyTo`/`CopyProperties`: builds the stream for `source`, and on success stores a
|
|
49
|
+
* `"fastTransfer"` handle at `outputHandleIndex` with the stream in the `HandleDataStore`. Returns `false`, storing
|
|
50
|
+
* nothing, when the stream exceeds `MAX_FAST_TRANSFER_BYTES` or the session's or user's handle data quota
|
|
51
|
+
* (`handleDataOwners`) has no room for it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function openFastTransferHandle(context: RopContext, outputHandleIndex: number, source: MapiObjectHandle, options: {
|
|
54
|
+
columns?: PropertyColumn[];
|
|
55
|
+
excludePropertyIds?: number[];
|
|
56
|
+
}): Promise<boolean>;
|
|
18
57
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RepoUtils } from "@rapidrest/service-core";
|
|
2
2
|
import { Folder } from "@rapidmx/restapi";
|
|
3
3
|
import type { MapiSessionContext } from "../MapiSessionManager.js";
|
|
4
|
+
import type { ExecuteBudget } from "./ExecuteBudget.js";
|
|
4
5
|
export interface FolderTargetInfo {
|
|
5
6
|
displayName: string;
|
|
6
7
|
unreadCount: number;
|
|
@@ -19,7 +20,7 @@ export interface FolderResolutionCache {
|
|
|
19
20
|
/** Resolves a target string into the display data a folder-table row needs. A `"folder:<uid>"` target whose
|
|
20
21
|
* `Folder` has since been deleted (soft-deleted or otherwise vanished) degrades to empty-looking values rather
|
|
21
22
|
* than throwing - the row simply won't be interesting to a client, not a reason to fail the whole ROP. */
|
|
22
|
-
export declare function resolveFolderInfo(mailboxUid: string, target: string, folderRepo: RepoUtils<any>, cache?: FolderResolutionCache): Promise<FolderTargetInfo>;
|
|
23
|
+
export declare function resolveFolderInfo(mailboxUid: string, target: string, folderRepo: RepoUtils<any>, cache?: FolderResolutionCache, budget?: ExecuteBudget): Promise<FolderTargetInfo>;
|
|
23
24
|
/**
|
|
24
25
|
* Resolves the direct children of `target`, as an array of the same target-string format, for
|
|
25
26
|
* `RopGetHierarchyTable`. Only `"virtual:root"`/`"virtual:ipmSubtree"` (this mailbox's top-level real
|
|
@@ -44,7 +45,7 @@ export declare function resolveFolderInfo(mailboxUid: string, target: string, fo
|
|
|
44
45
|
* that batch so the full list is fetched at most once per batch instead of once per row - see
|
|
45
46
|
* `FolderResolutionCache`'s own doc comment for why this isn't session state.
|
|
46
47
|
*/
|
|
47
|
-
export declare function resolveFolderChildren(mailboxUid: string, target: string, folderRepo: RepoUtils<any>, cache?: FolderResolutionCache): Promise<string[]>;
|
|
48
|
+
export declare function resolveFolderChildren(mailboxUid: string, target: string, folderRepo: RepoUtils<any>, cache?: FolderResolutionCache, budget?: ExecuteBudget): Promise<string[]>;
|
|
48
49
|
/**
|
|
49
50
|
* Returns `target`'s existing FID if `RopLogon` or an earlier `RopGetHierarchyTable` row already assigned
|
|
50
51
|
* one, otherwise assigns and remembers the next free small integer FID. This is what lets a client
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** How long an entry lives without being read - the same idle lifetime as a session. */
|
|
2
|
+
export declare const HANDLE_DATA_TTL_MS: number;
|
|
3
|
+
/** The most bytes this process keeps across all entries before evicting the least recently used ones. */
|
|
4
|
+
export declare const HANDLE_DATA_MAX_BYTES: number;
|
|
5
|
+
/**
|
|
6
|
+
* A process-local, byte-bounded LRU with TTL for large per-handle data: an opened stream's decoded message body
|
|
7
|
+
* and a FastTransfer handle's built stream. Keeping these out of the session JSON means an `Execute` doesn't
|
|
8
|
+
* re-serialize megabytes to Redis, and a `RopReadStream` doesn't re-fetch and re-parse the whole MIME message
|
|
9
|
+
* for every chunk.
|
|
10
|
+
*
|
|
11
|
+
* It is only a cache. Every caller can rebuild an entry from the handle's own (small) session state, so a miss
|
|
12
|
+
* (another replica served the previous request, eviction, TTL) costs one rebuild, never wrong data. Keys carry
|
|
13
|
+
* the handle's generation (see `assignHandle`), so a reused handle index never picks up a previous handle's data.
|
|
14
|
+
*/
|
|
15
|
+
export declare class HandleDataCache {
|
|
16
|
+
private readonly maxBytes;
|
|
17
|
+
private readonly ttlMs;
|
|
18
|
+
private readonly entries;
|
|
19
|
+
private totalBytes;
|
|
20
|
+
constructor(maxBytes?: number, ttlMs?: number);
|
|
21
|
+
get(key: string): Buffer | undefined;
|
|
22
|
+
/** Stores `value` under `key`. A value larger than the whole budget isn't cached at all. */
|
|
23
|
+
set(key: string, value: Buffer): void;
|
|
24
|
+
/** The size of the live entry at `key`, without refreshing it or changing its recency. */
|
|
25
|
+
sizeOf(key: string): number | undefined;
|
|
26
|
+
delete(key: string): void;
|
|
27
|
+
/** The bytes currently held, for tests and diagnostics. */
|
|
28
|
+
get size(): number;
|
|
29
|
+
}
|
|
30
|
+
/** The process-wide instance every handler shares. */
|
|
31
|
+
export declare const handleDataCache: HandleDataCache;
|
|
32
|
+
/** The cache key for the handle at `handleIndex`, as of its current generation. A generation is a random nonce
|
|
33
|
+
* (see `assignHandle`), so a key is never reused, not even by a request whose session changes were never saved. */
|
|
34
|
+
export declare function handleDataKey(sessionUid: string, handleIndex: number, generation: string | undefined): string;
|
|
35
|
+
/** The `HandleDataStore` key of the write stream at `handleIndex`. */
|
|
36
|
+
export declare function writeStreamKey(sessionUid: string, handleIndex: number, generation: string | undefined): string;
|
|
37
|
+
/** The most bytes of FastTransfer streams and write-stream chunks one session keeps stored at once. */
|
|
38
|
+
export declare const MAX_HANDLE_DATA_BYTES_PER_SESSION: number;
|
|
39
|
+
/** The most bytes of FastTransfer streams and write-stream chunks one user keeps stored at once, across all of their
|
|
40
|
+
* sessions. */
|
|
41
|
+
export declare const MAX_HANDLE_DATA_BYTES_PER_USER: number;
|
|
42
|
+
/** How long an owner index outlives its last write: a session's longest lifetime. */
|
|
43
|
+
export declare const HANDLE_DATA_INDEX_TTL_SECONDS: number;
|
|
44
|
+
/** The most chunks one write stream keeps. Each `RopWriteStream` stores one chunk, so this bounds the hash a stream
|
|
45
|
+
* builds up (and the work `readChunks` does) however small the writes are. */
|
|
46
|
+
export declare const MAX_CHUNKS_PER_STREAM = 8192;
|
|
47
|
+
/** How often, at most, an owner index that is over its quota is recounted from its members. Between recounts a write is
|
|
48
|
+
* checked against the index's running total alone, so a client hammering a full quota can't make every write walk
|
|
49
|
+
* every member. */
|
|
50
|
+
export declare const OWNER_RECOUNT_INTERVAL_MS = 1000;
|
|
51
|
+
/** An index that stored data is counted against: the keys it holds and the most bytes they may add up to. */
|
|
52
|
+
export interface HandleDataOwner {
|
|
53
|
+
index: string;
|
|
54
|
+
maxBytes: number;
|
|
55
|
+
}
|
|
56
|
+
/** The index of everything a session stores, used for its quota and to delete it all when the session ends. */
|
|
57
|
+
export declare function sessionHandleDataIndex(sessionUid: string): string;
|
|
58
|
+
/** The quota owners a write by `sessionUid` (belonging to `userUid`) counts against. */
|
|
59
|
+
export declare function handleDataOwners(sessionUid: string, userUid: string): HandleDataOwner[];
|
|
60
|
+
/**
|
|
61
|
+
* Where per-handle data that has to outlive one `Execute` lives: a built FastTransfer stream, and the chunks a write
|
|
62
|
+
* stream has accumulated. With Redis (`RedisHandleDataStore`) every replica sees the same data, so a client can page
|
|
63
|
+
* a stream out or finish writing a body through any pod. Without it (`MemoryHandleDataStore`) the data is only in
|
|
64
|
+
* this process.
|
|
65
|
+
*
|
|
66
|
+
* **Quotas.** A write given `owners` is recorded in each owner's index and refused (`false`, nothing stored) when it
|
|
67
|
+
* would take any owner past its `maxBytes`. Sizes are raw (unencoded) bytes. Every write costs O(1) per owner: each
|
|
68
|
+
* index keeps a running total, and a chunked stream keeps its own size (the end of its furthest chunk), so neither the
|
|
69
|
+
* stream's chunks nor the index's members are walked. A delete given the same `owners` takes the key's bytes off their
|
|
70
|
+
* totals. Data that disappears otherwise (TTL, eviction, `deleteOwnedBy` of another index, a delete without owners)
|
|
71
|
+
* keeps counting until a write that the total would refuse recounts the index from its live members, at most once per
|
|
72
|
+
* `OWNER_RECOUNT_INTERVAL_MS` per index. Totals only ever over-count, never under-count.
|
|
73
|
+
*/
|
|
74
|
+
export interface HandleDataStore {
|
|
75
|
+
get(key: string): Promise<Buffer | undefined>;
|
|
76
|
+
/** Stores `value` at `key`. `false` when an owner's quota refused it. */
|
|
77
|
+
set(key: string, value: Buffer, ttlSeconds: number, owners?: HandleDataOwner[]): Promise<boolean>;
|
|
78
|
+
/** Stores `data` as the chunk starting at byte `offset` of the stream at `key`. Writing the same offset again
|
|
79
|
+
* replaces the chunk, so a retried write never duplicates bytes or charges the quota twice. `false` when an owner's
|
|
80
|
+
* quota refused it, or the stream already has `MAX_CHUNKS_PER_STREAM` chunks and this is a new one. */
|
|
81
|
+
putChunk(key: string, offset: number, data: Buffer, ttlSeconds: number, owners?: HandleDataOwner[]): Promise<boolean>;
|
|
82
|
+
/** The first `size` bytes of the stream at `key`, assembled from its chunks, or `undefined` when a chunk is
|
|
83
|
+
* missing (expired or evicted) and the stream can't be reassembled exactly. */
|
|
84
|
+
readChunks(key: string, size: number): Promise<Buffer | undefined>;
|
|
85
|
+
/** Deletes the value or chunked stream at `key`, taking it off the totals of `owners`. */
|
|
86
|
+
delete(key: string, owners?: HandleDataOwner[]): Promise<void>;
|
|
87
|
+
/** Deletes everything recorded in the owner index `index`, and the index itself. */
|
|
88
|
+
deleteOwnedBy(index: string): Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
/** A `HandleDataStore` in this process only, on a `HandleDataCache` (so it is byte-bounded and can evict). */
|
|
91
|
+
export declare class MemoryHandleDataStore implements HandleDataStore {
|
|
92
|
+
private readonly cache;
|
|
93
|
+
private readonly indexes;
|
|
94
|
+
private readonly streams;
|
|
95
|
+
constructor(cache?: HandleDataCache);
|
|
96
|
+
get(key: string): Promise<Buffer | undefined>;
|
|
97
|
+
set(key: string, value: Buffer, _ttlSeconds: number, owners?: HandleDataOwner[]): Promise<boolean>;
|
|
98
|
+
putChunk(key: string, offset: number, data: Buffer, _ttlSeconds: number, owners?: HandleDataOwner[]): Promise<boolean>;
|
|
99
|
+
readChunks(key: string, size: number): Promise<Buffer | undefined>;
|
|
100
|
+
delete(key: string, owners?: HandleDataOwner[]): Promise<void>;
|
|
101
|
+
deleteOwnedBy(index: string): Promise<void>;
|
|
102
|
+
/** `true` while `key` still holds a value or its stream's first chunk (a stream missing that can't be read). */
|
|
103
|
+
private exists;
|
|
104
|
+
/** Records `key` at `bytes` in every owner index, unless that would take one past its quota. */
|
|
105
|
+
private admit;
|
|
106
|
+
/** `true` when `key` at `bytes` keeps `index` within `maxBytes`, recounting an index that looks full (rate limited). */
|
|
107
|
+
private fits;
|
|
108
|
+
}
|
|
109
|
+
/** Stores a value within its owners' quotas. KEYS: data key, then each owner index. ARGV: data key prefix, member name,
|
|
110
|
+
* index ttl, recount interval ms, base64 value, ttl, raw bytes, then each owner's max bytes. Returns 1 stored, 0 refused. */
|
|
111
|
+
export declare const SET_WITH_QUOTA_SCRIPT = "\nlocal function recount(index, prefix, indexTtl)\n local total = 0\n local members = redis.call('ZRANGE', index, 0, -1, 'WITHSCORES')\n for j = 1, #members, 2 do\n if redis.call('EXISTS', prefix .. members[j]) == 0 then\n redis.call('ZREM', index, members[j])\n else\n total = total + tonumber(members[j + 1])\n end\n end\n redis.call('SET', index .. '.total', tostring(total), 'EX', indexTtl)\n return total\nend\nlocal function admits(index, member, bytes, maxBytes, prefix, recountMs, indexTtl)\n local stored = redis.call('GET', index .. '.total')\n local fresh = not stored\n local total = stored and tonumber(stored) or recount(index, prefix, indexTtl)\n local old = tonumber(redis.call('ZSCORE', index, member) or 0)\n if total - old + bytes <= maxBytes then return true end\n if fresh or not redis.call('SET', index .. '.recount', '1', 'PX', recountMs, 'NX') then return false end\n total = recount(index, prefix, indexTtl)\n old = tonumber(redis.call('ZSCORE', index, member) or 0)\n return total - old + bytes <= maxBytes\nend\nlocal function record(index, member, bytes, indexTtl)\n local old = tonumber(redis.call('ZSCORE', index, member) or 0)\n redis.call('ZADD', index, tostring(bytes), member)\n redis.call('INCRBY', index .. '.total', tostring(bytes - old))\n redis.call('EXPIRE', index, indexTtl)\n redis.call('EXPIRE', index .. '.total', indexTtl)\nend\n\nlocal bytes = tonumber(ARGV[7])\nfor i = 2, #KEYS do\n if not admits(KEYS[i], ARGV[2], bytes, tonumber(ARGV[i + 6]), ARGV[1], ARGV[4], ARGV[3]) then return 0 end\nend\nredis.call('SETEX', KEYS[1], ARGV[6], ARGV[5])\nfor i = 2, #KEYS do record(KEYS[i], ARGV[2], bytes, ARGV[3]) end\nreturn 1\n";
|
|
112
|
+
/** Stores one write-stream chunk within its owners' quotas, in O(1): the hash keeps its size (the end of its furthest
|
|
113
|
+
* chunk) in the `size` field, and that size is what the owners are charged, so a chunk rewritten at the same offset
|
|
114
|
+
* isn't charged twice. KEYS: hash key, then each owner index. ARGV: data key prefix, member name, index ttl, recount
|
|
115
|
+
* interval ms, unpadded base64url chunk, ttl, raw chunk bytes, hash field (offset), max chunks, then each owner's max
|
|
116
|
+
* bytes. Returns 1 stored, 0 refused (quota, or a new chunk past the max chunks). */
|
|
117
|
+
export declare const PUT_CHUNK_WITH_QUOTA_SCRIPT = "\nlocal function recount(index, prefix, indexTtl)\n local total = 0\n local members = redis.call('ZRANGE', index, 0, -1, 'WITHSCORES')\n for j = 1, #members, 2 do\n if redis.call('EXISTS', prefix .. members[j]) == 0 then\n redis.call('ZREM', index, members[j])\n else\n total = total + tonumber(members[j + 1])\n end\n end\n redis.call('SET', index .. '.total', tostring(total), 'EX', indexTtl)\n return total\nend\nlocal function admits(index, member, bytes, maxBytes, prefix, recountMs, indexTtl)\n local stored = redis.call('GET', index .. '.total')\n local fresh = not stored\n local total = stored and tonumber(stored) or recount(index, prefix, indexTtl)\n local old = tonumber(redis.call('ZSCORE', index, member) or 0)\n if total - old + bytes <= maxBytes then return true end\n if fresh or not redis.call('SET', index .. '.recount', '1', 'PX', recountMs, 'NX') then return false end\n total = recount(index, prefix, indexTtl)\n old = tonumber(redis.call('ZSCORE', index, member) or 0)\n return total - old + bytes <= maxBytes\nend\nlocal function record(index, member, bytes, indexTtl)\n local old = tonumber(redis.call('ZSCORE', index, member) or 0)\n redis.call('ZADD', index, tostring(bytes), member)\n redis.call('INCRBY', index .. '.total', tostring(bytes - old))\n redis.call('EXPIRE', index, indexTtl)\n redis.call('EXPIRE', index .. '.total', indexTtl)\nend\n\nlocal field = ARGV[8]\nif redis.call('HEXISTS', KEYS[1], field) == 0 and redis.call('HLEN', KEYS[1]) > tonumber(ARGV[9]) then return 0 end\nlocal size = math.max(tonumber(redis.call('HGET', KEYS[1], 'size') or 0), tonumber(field) + tonumber(ARGV[7]))\nfor i = 2, #KEYS do\n if not admits(KEYS[i], ARGV[2], size, tonumber(ARGV[i + 8]), ARGV[1], ARGV[4], ARGV[3]) then return 0 end\nend\nredis.call('HSET', KEYS[1], field, ARGV[5], 'size', tostring(size))\nredis.call('EXPIRE', KEYS[1], ARGV[6])\nfor i = 2, #KEYS do record(KEYS[i], ARGV[2], size, ARGV[3]) end\nreturn 1\n";
|
|
118
|
+
/** Deletes one data key and takes it off its owners' totals. KEYS: data key, then each owner index. ARGV: member name. */
|
|
119
|
+
export declare const DELETE_WITH_OWNERS_SCRIPT = "\nredis.call('DEL', KEYS[1])\nfor i = 2, #KEYS do\n local old = redis.call('ZSCORE', KEYS[i], ARGV[1])\n if old then\n redis.call('ZREM', KEYS[i], ARGV[1])\n if redis.call('EXISTS', KEYS[i] .. '.total') == 1 then redis.call('INCRBY', KEYS[i] .. '.total', tostring(-tonumber(old))) end\n end\nend\nreturn 1\n";
|
|
120
|
+
/** Deletes every key an owner index names, then the index with its total and recount marker. KEYS: index. ARGV: data key
|
|
121
|
+
* prefix. Returns the members (unprefixed keys) it deleted, so the caller can drop its local copies too. */
|
|
122
|
+
export declare const DELETE_OWNED_SCRIPT = "\nlocal members = redis.call('ZRANGE', KEYS[1], 0, -1)\nfor _, member in ipairs(members) do\n redis.call('DEL', ARGV[1] .. member)\nend\nredis.call('DEL', KEYS[1], KEYS[1] .. '.total', KEYS[1] .. '.recount')\nreturn members\n";
|
|
123
|
+
/** The prefix of every handle data key in Redis. */
|
|
124
|
+
export declare const HANDLE_DATA_PREFIX = "mapi.handle.";
|
|
125
|
+
/**
|
|
126
|
+
* A `HandleDataStore` on a node-redis client, with this process's `HandleDataCache` in front for reads. Values are
|
|
127
|
+
* stored base64-encoded (a plain string works with any client configuration); a write stream is a hash of
|
|
128
|
+
* `offset -> chunk` plus its `size`. Owner indexes are sorted sets (`mapi.handle-owner.<index>`) of key -> raw bytes, with
|
|
129
|
+
* a running total beside each (see `OWNER_INDEX_LUA`).
|
|
130
|
+
*/
|
|
131
|
+
export declare class RedisHandleDataStore implements HandleDataStore {
|
|
132
|
+
private readonly client;
|
|
133
|
+
private readonly local;
|
|
134
|
+
constructor(client: any, local?: HandleDataCache);
|
|
135
|
+
get(key: string): Promise<Buffer | undefined>;
|
|
136
|
+
set(key: string, value: Buffer, ttlSeconds: number, owners?: HandleDataOwner[]): Promise<boolean>;
|
|
137
|
+
putChunk(key: string, offset: number, data: Buffer, ttlSeconds: number, owners?: HandleDataOwner[]): Promise<boolean>;
|
|
138
|
+
readChunks(key: string, size: number): Promise<Buffer | undefined>;
|
|
139
|
+
delete(key: string, owners?: HandleDataOwner[]): Promise<void>;
|
|
140
|
+
deleteOwnedBy(index: string): Promise<void>;
|
|
141
|
+
}
|
|
142
|
+
/** The Redis key of an owner index. */
|
|
143
|
+
export declare function ownerKey(index: string): string;
|
|
144
|
+
/** The store used when a `RopContext` doesn't carry one (unit tests, or a route built without a session manager). */
|
|
145
|
+
export declare const defaultHandleDataStore: HandleDataStore;
|
|
@@ -1,36 +1,30 @@
|
|
|
1
1
|
import type { RopContext } from "./RopHandler.js";
|
|
2
2
|
/**
|
|
3
3
|
* Handles a submitted `"IPM.Schedule.Meeting.Resp.{Pos,Neg,Tent}"` message - an attendee's own response to a
|
|
4
|
-
* meeting invite
|
|
5
|
-
*
|
|
4
|
+
* meeting invite. Delegated to from `RopSubmitMessageHandler` for that message-class prefix, instead of the ordinary
|
|
5
|
+
* mail or Appointment paths. Returns the ROP's `ReturnValue`.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* client from the invite it's responding to - see `GlobalObjectId.ts`'s own
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* between them.
|
|
7
|
+
* Mirrors restapi's `BaseCalendarEventRoute.respond()`. It correlates the response via `PidLidGlobalObjectId`
|
|
8
|
+
* (echoed by a real client from the invite it's responding to - see `GlobalObjectId.ts`) to **the caller's own
|
|
9
|
+
* copy** of the meeting: the query is scoped to the caller's `mailboxUid`, which is the access check. The
|
|
10
|
+
* organizer's copy (in another mailbox) is never touched here; the organizer's server applies the iTIP `REPLY` sent
|
|
11
|
+
* below, exactly as for a REST response. A native Outlook `GlobalObjectId` decodes to the uppercase hex form of its
|
|
12
|
+
* iCalendar UID, which is matched in either case.
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* **One occurrence of a recurring meeting** (the `GlobalObjectId`'s instance date is set): the caller's exception
|
|
15
|
+
* copy for that date is used when there is one, comparing dates in the event's own time zone. Without one, the
|
|
16
|
+
* series is left alone - responding to one occurrence must not change or delete every other occurrence. Declining
|
|
17
|
+
* adds the occurrence to the caller's series `recurrenceRule.exceptions` (so it leaves their calendar); accepting or
|
|
18
|
+
* tentatively accepting changes nothing locally. Either way the `REPLY` names just that occurrence (`RECURRENCE-ID`).
|
|
20
19
|
*
|
|
21
|
-
* **The
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* `
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* actual authorization gate is the attendee-membership check a few lines down (`callerAddresses.has(...)`),
|
|
31
|
-
* derived entirely from *this* caller's own resolved mailbox record, not from anything the client supplied -
|
|
32
|
-
* a crafted `PidLidGlobalObjectId` can at best name a real `icalUid` it doesn't already know the value of
|
|
33
|
-
* (astronomically unlikely against a random UUID) and still can't mutate an event unless the caller's own
|
|
34
|
-
* mailbox is genuinely a listed attendee on it.
|
|
20
|
+
* **The whole meeting** (no instance date, or a copy that doesn't recur): the caller's `Attendee.responseStatus` is
|
|
21
|
+
* recorded; declining soft-deletes the caller's copy instead. Declining an occurrence through its exception copy also
|
|
22
|
+
* adds that occurrence to the series' exceptions, so the series doesn't show it again. Every update is versioned
|
|
23
|
+
* (`asEntity`), so a concurrent change fails the response instead of being overwritten.
|
|
24
|
+
*
|
|
25
|
+
* The `REPLY` (restapi's own `buildEventIcs`) goes out through `sendOrThrow`, so a transport that rejects it is
|
|
26
|
+
* reported as `MAPI_E_CALL_FAILED` instead of success; the response itself stays recorded. An organizer or attendee
|
|
27
|
+
* address that isn't a plain SMTP address is not mailed at all. A response that can't be matched to a meeting the
|
|
28
|
+
* caller attends is an error (`MAPI_E_NOT_FOUND`/`MAPI_E_INVALID_PARAMETER`), not a silent success.
|
|
35
29
|
*/
|
|
36
|
-
export declare function submitMeetingResponse(messageClass: string, draftProperties: Record<string, string>, context: RopContext): Promise<
|
|
30
|
+
export declare function submitMeetingResponse(messageClass: string, draftProperties: Record<string, string>, context: RopContext): Promise<number>;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { RepoUtils } from "@rapidrest/service-core";
|
|
2
2
|
import { type BlobStore } from "@rapidmx/restapi";
|
|
3
|
+
import type { MapiObjectHandle } from "../MapiSessionManager.js";
|
|
4
|
+
import type { ExecuteBudget } from "./ExecuteBudget.js";
|
|
5
|
+
import type { RopContext } from "./RopHandler.js";
|
|
3
6
|
/** `PidTagBody` (`[MS-OXPROPS]`): property ID `0x1000`, `PtypString`. The only streamable property this
|
|
4
7
|
* pragmatic subset supports via `RopOpenStream`/`RopReadStream` - `PidTagHtml`/`PidTagRtfCompressed` are a
|
|
5
8
|
* documented gap (see `RopOpenStreamHandler`'s own doc comment). */
|
|
@@ -16,11 +19,17 @@ export declare const PID_TAG_BODY = 4096;
|
|
|
16
19
|
* `sanitizedHtmlBlobKey` - `PidTagBody` is specifically the plain-text body per `[MS-OXPROPS]`, unlike EAS's
|
|
17
20
|
* `Body` element, which can carry either format tagged by its own `Type` field.
|
|
18
21
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* a raw `Buffer` stored there wouldn't survive it (the same class of gap this codebase already documented for
|
|
22
|
-
* `Date` fields, see `MapiSessionContext`'s own doc comment). A documented, pragmatic trade-off: a large body
|
|
23
|
-
* read across several `RopReadStream` calls re-parses the MIME source each time rather than once - correct,
|
|
24
|
-
* not byte-perfect-efficient.
|
|
22
|
+
* Does no caching itself - `loadStreamBody` below is what the stream ROPs call. With a `budget`, nothing is fetched
|
|
23
|
+
* once its bytes are used up, and the raw message's size is charged before it is parsed.
|
|
25
24
|
*/
|
|
26
|
-
export declare function resolveMessageBodyBytes(target: string, messageRepo: RepoUtils<any>, blobStore: BlobStore): Promise<Buffer>;
|
|
25
|
+
export declare function resolveMessageBodyBytes(target: string, messageRepo: RepoUtils<any>, blobStore: BlobStore, budget?: ExecuteBudget): Promise<Buffer>;
|
|
26
|
+
/**
|
|
27
|
+
* The body bytes for the read stream at `handleIndex`. Parsed once and kept in `HandleDataCache` (outside the
|
|
28
|
+
* session JSON) for the life of the handle, so reading a large body in many `RopReadStream` chunks costs one
|
|
29
|
+
* fetch and one MIME parse instead of one per chunk. A cache miss (eviction, or a request served by another
|
|
30
|
+
* replica) just resolves the body again: a stored message's body never changes, so the bytes are the same.
|
|
31
|
+
*
|
|
32
|
+
* Within one `Execute`, streams opened on the same message share a single parse (`ExecuteBudget.bodies`), and each
|
|
33
|
+
* parse's bytes count against the request's byte budget.
|
|
34
|
+
*/
|
|
35
|
+
export declare function loadStreamBody(context: RopContext, handleIndex: number, stream: MapiObjectHandle): Promise<Buffer>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { RepoUtils } from "@rapidrest/service-core";
|
|
2
2
|
import type { MapiSessionContext } from "../MapiSessionManager.js";
|
|
3
|
+
import type { ExecuteBudget } from "./ExecuteBudget.js";
|
|
4
|
+
import { type RepoSort } from "./RepoPaging.js";
|
|
3
5
|
/**
|
|
4
6
|
* The `RopGetContentsTable` analog of `FolderTarget.ts`: resolves a `"message:<uid>"` row target (see
|
|
5
7
|
* `RopGetContentsTableHandler`'s own doc comment for why contents-table rows use this prefix instead of
|
|
@@ -19,9 +21,11 @@ export interface MessageTargetInfo {
|
|
|
19
21
|
* (soft-deleted or otherwise), the same "don't fail the whole ROP over one stale row" principle
|
|
20
22
|
* `FolderTarget.resolveFolderInfo` already applies. */
|
|
21
23
|
export declare function resolveMessageInfo(target: string, messageRepo: RepoUtils<any>): Promise<MessageTargetInfo>;
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
/** The order a folder's messages are listed in: newest first, ties broken by `uid` so paging is stable. */
|
|
25
|
+
export declare const MESSAGE_SORT: RepoSort;
|
|
26
|
+
/** Resolves the messages directly in `folderUid`, newest first and capped at `MAX_COLLECTION_ROWS`, as
|
|
27
|
+
* `"message:<uid>"` target strings. */
|
|
28
|
+
export declare function resolveFolderMessages(folderUid: string, messageRepo: RepoUtils<any>, budget?: ExecuteBudget): Promise<string[]>;
|
|
25
29
|
/**
|
|
26
30
|
* Returns `target`'s existing MID if an earlier `RopQueryRows` row already assigned one, otherwise assigns and
|
|
27
31
|
* remembers the next free small integer MID. The exact `FolderTarget.assignOrGetFid` pattern, adapted for
|
|
@@ -36,3 +40,6 @@ export declare function resolveFolderMessages(folderUid: string, messageRepo: Re
|
|
|
36
40
|
* that pages through many messages.
|
|
37
41
|
*/
|
|
38
42
|
export declare function assignOrGetMid(session: MapiSessionContext, target: string): number;
|
|
43
|
+
/** The most MIDs one session keeps mapped. A session paging through a very large mailbox would otherwise grow its
|
|
44
|
+
* MID maps - saved with the session on every `Execute` - without bound. */
|
|
45
|
+
export declare const MAX_MESSAGE_IDS = 20000;
|
|
@@ -11,6 +11,10 @@ export interface PropertyName {
|
|
|
11
11
|
lid?: number;
|
|
12
12
|
name?: string;
|
|
13
13
|
}
|
|
14
|
+
/** The most distinct named properties one session registers. The registry is saved with the session on every
|
|
15
|
+
* `Execute`, and a name can be up to 255 bytes, so letting a client fill the whole `0x8000`-`0xFFFF` ID space
|
|
16
|
+
* would make every request carry megabytes of session state. Real clients register a few hundred at most. */
|
|
17
|
+
export declare const MAX_NAMED_PROPERTIES_PER_SESSION = 4096;
|
|
14
18
|
/**
|
|
15
19
|
* Returns `propertyName`'s existing numeric property ID if an earlier `RopGetPropertyIdsFromNames` call in
|
|
16
20
|
* this session already assigned one, otherwise assigns and remembers the next free ID starting at
|
|
@@ -28,8 +32,8 @@ export interface PropertyName {
|
|
|
28
32
|
* lifetime, so a linear scan repeated once per property per row is quadratic over a session that resolves many
|
|
29
33
|
* named properties.
|
|
30
34
|
*
|
|
31
|
-
* Once `nextNamedPropertyId` would exceed `LAST_NAMED_PROPERTY_ID` (
|
|
32
|
-
*
|
|
35
|
+
* Once `nextNamedPropertyId` would exceed `LAST_NAMED_PROPERTY_ID` (`MAX_NAMED_PROPERTIES_PER_SESSION` distinct
|
|
36
|
+
* names already registered this session), a *new*
|
|
33
37
|
* name can no longer be assigned a real ID; this returns `0x0000` for it instead of throwing, the exact value
|
|
34
38
|
* `[MS-OXCPRPT]`'s own `RopGetPropertyIdsFromNames` processing rules already use for "this `PropertyName`
|
|
35
39
|
* could not be resolved" (the same value this pragmatic subset already produces for a `Kind = 0xFF` entry) -
|
|
@@ -57,4 +57,4 @@ export interface ResolutionCache extends FolderResolutionCache {
|
|
|
57
57
|
export declare function resolvePropertyValues(target: string, columns: {
|
|
58
58
|
propertyId: number;
|
|
59
59
|
propertyType: PropertyType;
|
|
60
|
-
}[], context: Pick<RopContext, "mailboxUid" | "session" | "folderRepo" | "messageRepo" | "calendarEventRepo" | "mailboxRepo" | "contactRepo" | "taskRepo" | "labelRepo">, cache?: ResolutionCache): Promise<PropertyValueData[]>;
|
|
60
|
+
}[], context: Pick<RopContext, "mailboxUid" | "session" | "folderRepo" | "messageRepo" | "calendarEventRepo" | "mailboxRepo" | "contactRepo" | "taskRepo" | "labelRepo" | "budget">, cache?: ResolutionCache): Promise<PropertyValueData[]>;
|