@rapidmx/mapi-plugin 1.0.0-beta.10
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/LICENSE +382 -0
- package/README.md +60 -0
- package/dist/lib/BaseMapiEmsmdbRoute.js +489 -0
- package/dist/lib/BaseMapiEmsmdbRoute.js.map +1 -0
- package/dist/lib/BaseMapiNspiRoute.js +114 -0
- package/dist/lib/BaseMapiNspiRoute.js.map +1 -0
- package/dist/lib/MapiSessionManager.js +475 -0
- package/dist/lib/MapiSessionManager.js.map +1 -0
- package/dist/lib/RegexPatternUtils.js +30 -0
- package/dist/lib/RegexPatternUtils.js.map +1 -0
- package/dist/lib/RopDispatcher.js +145 -0
- package/dist/lib/RopDispatcher.js.map +1 -0
- package/dist/lib/codec/AppointmentRecurrence.js +315 -0
- package/dist/lib/codec/AppointmentRecurrence.js.map +1 -0
- package/dist/lib/codec/BufferCursor.js +242 -0
- package/dist/lib/codec/BufferCursor.js.map +1 -0
- package/dist/lib/codec/GlobalObjectId.js +98 -0
- package/dist/lib/codec/GlobalObjectId.js.map +1 -0
- package/dist/lib/codec/MapiGuid.js +41 -0
- package/dist/lib/codec/MapiGuid.js.map +1 -0
- package/dist/lib/codec/MapiTimeZone.js +126 -0
- package/dist/lib/codec/MapiTimeZone.js.map +1 -0
- package/dist/lib/codec/PropertyValue.js +240 -0
- package/dist/lib/codec/PropertyValue.js.map +1 -0
- package/dist/lib/codec/RopBuffer.js +38 -0
- package/dist/lib/codec/RopBuffer.js.map +1 -0
- package/dist/lib/codec/TypedString.js +54 -0
- package/dist/lib/codec/TypedString.js.map +1 -0
- package/dist/lib/index.js +99 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/mongo/MapiEmsmdbRouteMongo.js +85 -0
- package/dist/lib/mongo/MapiEmsmdbRouteMongo.js.map +1 -0
- package/dist/lib/mongo/MapiNspiRouteMongo.js +33 -0
- package/dist/lib/mongo/MapiNspiRouteMongo.js.map +1 -0
- package/dist/lib/mongo/index.js +7 -0
- package/dist/lib/mongo/index.js.map +1 -0
- package/dist/lib/mongo.js +6 -0
- package/dist/lib/mongo.js.map +1 -0
- package/dist/lib/nspi/NspiBindHandler.js +54 -0
- package/dist/lib/nspi/NspiBindHandler.js.map +1 -0
- package/dist/lib/nspi/NspiCodec.js +106 -0
- package/dist/lib/nspi/NspiCodec.js.map +1 -0
- package/dist/lib/nspi/NspiGetMatchesHandler.js +142 -0
- package/dist/lib/nspi/NspiGetMatchesHandler.js.map +1 -0
- package/dist/lib/rop/AddressList.js +114 -0
- package/dist/lib/rop/AddressList.js.map +1 -0
- package/dist/lib/rop/CalendarEventTarget.js +30 -0
- package/dist/lib/rop/CalendarEventTarget.js.map +1 -0
- package/dist/lib/rop/CalendarNamedProperties.js +57 -0
- package/dist/lib/rop/CalendarNamedProperties.js.map +1 -0
- package/dist/lib/rop/ContactTarget.js +19 -0
- package/dist/lib/rop/ContactTarget.js.map +1 -0
- 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 +195 -0
- package/dist/lib/rop/FastTransferStream.js.map +1 -0
- package/dist/lib/rop/FolderTarget.js +118 -0
- package/dist/lib/rop/FolderTarget.js.map +1 -0
- package/dist/lib/rop/HandleDataCache.js +412 -0
- package/dist/lib/rop/HandleDataCache.js.map +1 -0
- package/dist/lib/rop/MeetingMessageClassHandler.js +241 -0
- package/dist/lib/rop/MeetingMessageClassHandler.js.map +1 -0
- package/dist/lib/rop/MessageBodyStream.js +68 -0
- package/dist/lib/rop/MessageBodyStream.js.map +1 -0
- package/dist/lib/rop/MessageTarget.js +57 -0
- package/dist/lib/rop/MessageTarget.js.map +1 -0
- package/dist/lib/rop/NamedPropertyRegistry.js +69 -0
- package/dist/lib/rop/NamedPropertyRegistry.js.map +1 -0
- package/dist/lib/rop/PropertyResolvers.js +353 -0
- package/dist/lib/rop/PropertyResolvers.js.map +1 -0
- 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 +55 -0
- package/dist/lib/rop/RopCreateMessageHandler.js.map +1 -0
- package/dist/lib/rop/RopDeleteFolderHandler.js +174 -0
- package/dist/lib/rop/RopDeleteFolderHandler.js.map +1 -0
- package/dist/lib/rop/RopDeleteMessagesHandler.js +110 -0
- package/dist/lib/rop/RopDeleteMessagesHandler.js.map +1 -0
- package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js +61 -0
- package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js.map +1 -0
- package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js +60 -0
- package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js.map +1 -0
- package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js +76 -0
- package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js.map +1 -0
- package/dist/lib/rop/RopGetContentsTableHandler.js +55 -0
- package/dist/lib/rop/RopGetContentsTableHandler.js.map +1 -0
- package/dist/lib/rop/RopGetHierarchyTableHandler.js +42 -0
- package/dist/lib/rop/RopGetHierarchyTableHandler.js.map +1 -0
- package/dist/lib/rop/RopGetPropertiesSpecificHandler.js +56 -0
- package/dist/lib/rop/RopGetPropertiesSpecificHandler.js.map +1 -0
- package/dist/lib/rop/RopGetPropertyIdsFromNamesHandler.js +92 -0
- package/dist/lib/rop/RopGetPropertyIdsFromNamesHandler.js.map +1 -0
- package/dist/lib/rop/RopHandler.js +6 -0
- package/dist/lib/rop/RopHandler.js.map +1 -0
- package/dist/lib/rop/RopLogonHandler.js +135 -0
- package/dist/lib/rop/RopLogonHandler.js.map +1 -0
- package/dist/lib/rop/RopOpenFolderHandler.js +48 -0
- package/dist/lib/rop/RopOpenFolderHandler.js.map +1 -0
- package/dist/lib/rop/RopOpenMessageHandler.js +83 -0
- package/dist/lib/rop/RopOpenMessageHandler.js.map +1 -0
- package/dist/lib/rop/RopOpenStreamHandler.js +84 -0
- package/dist/lib/rop/RopOpenStreamHandler.js.map +1 -0
- package/dist/lib/rop/RopQueryRowsHandler.js +120 -0
- package/dist/lib/rop/RopQueryRowsHandler.js.map +1 -0
- package/dist/lib/rop/RopReadStreamHandler.js +73 -0
- package/dist/lib/rop/RopReadStreamHandler.js.map +1 -0
- package/dist/lib/rop/RopReleaseHandler.js +23 -0
- package/dist/lib/rop/RopReleaseHandler.js.map +1 -0
- package/dist/lib/rop/RopSaveChangesMessageHandler.js +269 -0
- package/dist/lib/rop/RopSaveChangesMessageHandler.js.map +1 -0
- package/dist/lib/rop/RopSetColumnsHandler.js +50 -0
- package/dist/lib/rop/RopSetColumnsHandler.js.map +1 -0
- package/dist/lib/rop/RopSetPropertiesHandler.js +173 -0
- package/dist/lib/rop/RopSetPropertiesHandler.js.map +1 -0
- package/dist/lib/rop/RopSubmitMessageHandler.js +342 -0
- package/dist/lib/rop/RopSubmitMessageHandler.js.map +1 -0
- package/dist/lib/rop/RopWriteStreamHandler.js +72 -0
- package/dist/lib/rop/RopWriteStreamHandler.js.map +1 -0
- package/dist/lib/rop/TaskNamedProperties.js +29 -0
- package/dist/lib/rop/TaskNamedProperties.js.map +1 -0
- package/dist/lib/rop/TaskTarget.js +15 -0
- package/dist/lib/rop/TaskTarget.js.map +1 -0
- package/dist/lib/rop/TransportSend.js +16 -0
- package/dist/lib/rop/TransportSend.js.map +1 -0
- package/dist/lib/sql/MapiEmsmdbRouteSQL.js +85 -0
- package/dist/lib/sql/MapiEmsmdbRouteSQL.js.map +1 -0
- package/dist/lib/sql/MapiNspiRouteSQL.js +33 -0
- package/dist/lib/sql/MapiNspiRouteSQL.js.map +1 -0
- package/dist/lib/sql/index.js +7 -0
- package/dist/lib/sql/index.js.map +1 -0
- package/dist/lib/sql.js +6 -0
- package/dist/lib/sql.js.map +1 -0
- package/dist/types/BaseMapiEmsmdbRoute.d.ts +139 -0
- package/dist/types/BaseMapiNspiRoute.d.ts +38 -0
- package/dist/types/MapiSessionManager.d.ts +285 -0
- package/dist/types/RegexPatternUtils.d.ts +14 -0
- package/dist/types/RopDispatcher.d.ts +53 -0
- package/dist/types/codec/AppointmentRecurrence.d.ts +14 -0
- package/dist/types/codec/BufferCursor.d.ts +92 -0
- package/dist/types/codec/GlobalObjectId.d.ts +15 -0
- package/dist/types/codec/MapiGuid.d.ts +6 -0
- package/dist/types/codec/MapiTimeZone.d.ts +7 -0
- package/dist/types/codec/PropertyValue.d.ts +87 -0
- package/dist/types/codec/RopBuffer.d.ts +32 -0
- package/dist/types/codec/TypedString.d.ts +17 -0
- package/dist/types/index.d.ts +94 -0
- package/dist/types/mongo/MapiEmsmdbRouteMongo.d.ts +20 -0
- package/dist/types/mongo/MapiNspiRouteMongo.d.ts +13 -0
- package/dist/types/mongo/index.d.ts +2 -0
- package/dist/types/mongo.d.ts +1 -0
- package/dist/types/nspi/NspiBindHandler.d.ts +22 -0
- package/dist/types/nspi/NspiCodec.d.ts +59 -0
- package/dist/types/nspi/NspiGetMatchesHandler.d.ts +26 -0
- package/dist/types/rop/AddressList.d.ts +48 -0
- package/dist/types/rop/CalendarEventTarget.d.ts +33 -0
- package/dist/types/rop/CalendarNamedProperties.d.ts +37 -0
- package/dist/types/rop/ContactTarget.d.ts +29 -0
- 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 +57 -0
- package/dist/types/rop/FolderTarget.d.ts +64 -0
- package/dist/types/rop/HandleDataCache.d.ts +145 -0
- package/dist/types/rop/MeetingMessageClassHandler.d.ts +30 -0
- package/dist/types/rop/MessageBodyStream.d.ts +35 -0
- package/dist/types/rop/MessageTarget.d.ts +45 -0
- package/dist/types/rop/NamedPropertyRegistry.d.ts +50 -0
- package/dist/types/rop/PropertyResolvers.d.ts +86 -0
- 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 +24 -0
- package/dist/types/rop/RopDeleteFolderHandler.d.ts +53 -0
- package/dist/types/rop/RopDeleteMessagesHandler.d.ts +43 -0
- package/dist/types/rop/RopFastTransferSourceCopyPropertiesHandler.d.ts +25 -0
- package/dist/types/rop/RopFastTransferSourceCopyToHandler.d.ts +25 -0
- package/dist/types/rop/RopFastTransferSourceGetBufferHandler.d.ts +22 -0
- package/dist/types/rop/RopGetContentsTableHandler.d.ts +25 -0
- package/dist/types/rop/RopGetHierarchyTableHandler.d.ts +18 -0
- package/dist/types/rop/RopGetPropertiesSpecificHandler.d.ts +26 -0
- package/dist/types/rop/RopGetPropertyIdsFromNamesHandler.d.ts +23 -0
- package/dist/types/rop/RopHandler.d.ts +106 -0
- package/dist/types/rop/RopLogonHandler.d.ts +31 -0
- package/dist/types/rop/RopOpenFolderHandler.d.ts +20 -0
- package/dist/types/rop/RopOpenMessageHandler.d.ts +28 -0
- package/dist/types/rop/RopOpenStreamHandler.d.ts +28 -0
- package/dist/types/rop/RopQueryRowsHandler.d.ts +43 -0
- package/dist/types/rop/RopReadStreamHandler.d.ts +20 -0
- package/dist/types/rop/RopReleaseHandler.d.ts +15 -0
- package/dist/types/rop/RopSaveChangesMessageHandler.d.ts +59 -0
- package/dist/types/rop/RopSetColumnsHandler.d.ts +21 -0
- package/dist/types/rop/RopSetPropertiesHandler.d.ts +38 -0
- package/dist/types/rop/RopSubmitMessageHandler.d.ts +80 -0
- package/dist/types/rop/RopWriteStreamHandler.d.ts +31 -0
- package/dist/types/rop/TaskNamedProperties.d.ts +24 -0
- package/dist/types/rop/TaskTarget.d.ts +24 -0
- package/dist/types/rop/TransportSend.d.ts +10 -0
- package/dist/types/sql/MapiEmsmdbRouteSQL.d.ts +20 -0
- package/dist/types/sql/MapiNspiRouteSQL.d.ts +13 -0
- package/dist/types/sql/index.d.ts +2 -0
- package/dist/types/sql.d.ts +1 -0
- package/package.json +104 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { SimpleEntity } from "@rapidrest/service-core";
|
|
2
|
+
import { type HandleDataStore } from "./rop/HandleDataCache.js";
|
|
3
|
+
/** A pragmatic idle-session lifetime. The spec leaves session lifetime to server policy, not a fixed wire
|
|
4
|
+
* value - a real client re-establishes (`Connect`) transparently whenever its session context has expired. */
|
|
5
|
+
export declare const SESSION_TTL_SECONDS: number;
|
|
6
|
+
/** The longest a session may live, however active it stays. A client simply `Connect`s again afterwards. */
|
|
7
|
+
export declare const MAX_SESSION_LIFETIME_MS: number;
|
|
8
|
+
/** The most sessions one user may hold at once. `Connect` past this ends that user's least recently used session (each
|
|
9
|
+
* `Execute` refreshes its session's place - see `MapiSessionManager.touch`). */
|
|
10
|
+
export declare const MAX_SESSIONS_PER_USER = 20;
|
|
11
|
+
/** The largest a serialized session may grow. Everything that grows with use is capped on its own (handles by their
|
|
12
|
+
* one-byte index, named properties, MIDs; write streams live outside the session); this is the backstop, and a save
|
|
13
|
+
* past it fails with `MAPI_E_TOO_BIG` rather than storing megabytes every later request has to load. */
|
|
14
|
+
export declare const MAX_SESSION_BYTES: number;
|
|
15
|
+
/** How long an `Execute`'s session lock lives without being renewed. A running `Execute` renews it every
|
|
16
|
+
* `SESSION_LOCK_RENEW_MS`, so only a crashed pod's lock expires. The session save is still compare-and-set, so an
|
|
17
|
+
* expired lock can't corrupt state. */
|
|
18
|
+
export declare const SESSION_LOCK_TTL_MS: number;
|
|
19
|
+
/** How often a running `Execute` renews its session lock and in-progress marker. */
|
|
20
|
+
export declare const SESSION_LOCK_RENEW_MS: number;
|
|
21
|
+
/** Tags what a ROP-assigned integer handle (the `ServerObjectHandleTable` index space) refers to.
|
|
22
|
+
* `entityUid` for a `"folder"` handle is one of `session.folderIds`' own value strings (`"virtual:<name>"` or
|
|
23
|
+
* `"folder:<uid>"`), not a bare UID - the same format throughout avoids a second parallel encoding.
|
|
24
|
+
*
|
|
25
|
+
* A `"table"` handle's `columns`/`cursor` hold `RopSetColumns`/`RopQueryRows` state. A hierarchy table carries its
|
|
26
|
+
* resolved child-folder targets in `rows`. A contents table carries no rows at all: `contentsKind` says which repo
|
|
27
|
+
* its folder's items live in and `RopQueryRows` reads just the requested window from the database (see
|
|
28
|
+
* `ContentsTable.ts`), so a large folder never lands in session state.
|
|
29
|
+
*
|
|
30
|
+
* A `"stream"` handle's `entityUid` is the `"message:<uid>"` target its content was opened from,
|
|
31
|
+
* `propertyId`/`propertyType` the `PropertyTag` `RopOpenStream` opened (this pragmatic subset only ever supports
|
|
32
|
+
* `PidTagBody`/`PtypString`, see `MessageBodyStream.ts`), and `streamPosition` how many bytes `RopReadStream` has
|
|
33
|
+
* already returned. A read stream's decoded body lives in `HandleDataCache`, not here. A write stream (opened
|
|
34
|
+
* `ReadWrite`/`Create` against a `RopCreateMessage` draft's `PidTagBody`) records the draft it belongs to in
|
|
35
|
+
* `writeTargetHandleIndex`/`writeTargetGeneration`, and how many bytes have been written in `writeSize`. The bytes
|
|
36
|
+
* themselves are chunks in the `HandleDataStore` (see `RopWriteStreamHandler`), not session state.
|
|
37
|
+
*
|
|
38
|
+
* A `"message"` handle from `RopCreateMessage` (a draft not yet `RopSaveChangesMessage`d) has `entityUid: ""`
|
|
39
|
+
* and instead carries `draftFolderUid` (the folder it will belong to) and `draftProperties` (the small,
|
|
40
|
+
* well-known set of properties this pragmatic subset's `RopSetProperties` tracks - Subject/DisplayTo/
|
|
41
|
+
* DisplayCc/DisplayBcc/an inline `PidTagBody`, each coerced to a plain string, keyed by decimal `PropertyId` -
|
|
42
|
+
* a string key because a JSON-object key is always a string regardless of how it's written). Buffers are stored
|
|
43
|
+
* as base64 and dates as ISO strings since sessions round-trip through `JSON.stringify`/`JSON.parse`.
|
|
44
|
+
*
|
|
45
|
+
* A `"fastTransfer"` handle (`RopFastTransferSourceCopyTo`/`CopyProperties`'s output handle) keeps only what is
|
|
46
|
+
* needed to rebuild its stream (`transferSourceType`, `transferColumns`/`transferExcludeIds`) plus the paging
|
|
47
|
+
* cursor `transferPosition`; the built stream itself lives in `HandleDataCache`.
|
|
48
|
+
*
|
|
49
|
+
* `generation` is a random nonce per assignment (see `assignHandle`), so data keyed by handle index can tell a handle
|
|
50
|
+
* apart from a later one that reuses the same index - including one assigned by a request whose session changes were
|
|
51
|
+
* never saved, which a per-session counter would hand out again. */
|
|
52
|
+
export interface MapiObjectHandle {
|
|
53
|
+
type: "logon" | "folder" | "message" | "table" | "stream" | "fastTransfer";
|
|
54
|
+
entityUid: string;
|
|
55
|
+
generation?: string;
|
|
56
|
+
rows?: string[];
|
|
57
|
+
contentsKind?: "message" | "calendarEvent" | "contact" | "task";
|
|
58
|
+
columns?: {
|
|
59
|
+
propertyId: number;
|
|
60
|
+
propertyType: number;
|
|
61
|
+
}[];
|
|
62
|
+
cursor?: number;
|
|
63
|
+
propertyId?: number;
|
|
64
|
+
propertyType?: number;
|
|
65
|
+
streamPosition?: number;
|
|
66
|
+
draftFolderUid?: string;
|
|
67
|
+
draftProperties?: Record<string, string>;
|
|
68
|
+
writeTargetHandleIndex?: number;
|
|
69
|
+
writeTargetGeneration?: string;
|
|
70
|
+
writeSize?: number;
|
|
71
|
+
transferSourceType?: "folder" | "message";
|
|
72
|
+
transferColumns?: {
|
|
73
|
+
propertyId: number;
|
|
74
|
+
propertyType: number;
|
|
75
|
+
}[];
|
|
76
|
+
transferExcludeIds?: number[];
|
|
77
|
+
transferPosition?: number;
|
|
78
|
+
/** Set on a draft message once `RopSubmitMessage` has tried to send it, whatever the outcome. A submitted draft
|
|
79
|
+
* can't be submitted again or changed. */
|
|
80
|
+
submitted?: boolean;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The MAPI/HTTP `Session Context` (`[MS-OXCMAPIHTTP]` §3.1.1.1): everything a `Connect`-established session
|
|
84
|
+
* needs across subsequent `Execute` requests. Never persisted to a real database - purely an ephemeral,
|
|
85
|
+
* TTL-bound store entry (see `MapiSessionManager` below), so this deliberately extends `SimpleEntity` (just a
|
|
86
|
+
* `uid`) rather than this library's own `BaseEntity`.
|
|
87
|
+
*
|
|
88
|
+
* `createdAt` is a plain ISO-8601 string, not a `Date`, since sessions round-trip through JSON. `version` is
|
|
89
|
+
* bumped on every successful save and compared on the next one - see `MapiSessionManager.save()`.
|
|
90
|
+
*/
|
|
91
|
+
export declare class MapiSessionContext extends SimpleEntity {
|
|
92
|
+
mailboxUid: string;
|
|
93
|
+
userUid: string;
|
|
94
|
+
version: number;
|
|
95
|
+
handles: Record<number, MapiObjectHandle>;
|
|
96
|
+
nextHandleIndex: number;
|
|
97
|
+
createdAt: string;
|
|
98
|
+
/** This session's FID assignments for the 13 `RopLogon` special folders, keyed by FID (decimal string),
|
|
99
|
+
* valued `"virtual:<name>"` or `"folder:<uid>"` - see `RopLogonHandler`'s own doc comment. Populated by
|
|
100
|
+
* `RopLogon`, read back by a later `RopOpenFolder`. */
|
|
101
|
+
folderIds: Record<string, string>;
|
|
102
|
+
/** The reverse of `folderIds` (target -> FID) plus a monotonic counter, maintained alongside it by
|
|
103
|
+
* `FolderTarget.assignOrGetFid`/`RopLogonHandler` so "does this target already have a FID" is an O(1)
|
|
104
|
+
* lookup instead of a linear scan of `folderIds` repeated once per table row. */
|
|
105
|
+
folderTargetIds: Record<string, number>;
|
|
106
|
+
nextFolderId: number;
|
|
107
|
+
/** This session's MID assignments, keyed by MID (decimal string), valued `"message:<uid>"` - the message
|
|
108
|
+
* analog of `folderIds` above. A MID only ever comes into existence lazily, the first time a `RopQueryRows`
|
|
109
|
+
* row exposes a message's `PidTagMid` column (see `MessageTarget.assignOrGetMid`), read back by a later
|
|
110
|
+
* `RopOpenMessage`. */
|
|
111
|
+
messageIds: Record<string, string>;
|
|
112
|
+
/** The reverse of `messageIds` (target -> MID) plus a monotonic counter. */
|
|
113
|
+
messageTargetIds: Record<string, number>;
|
|
114
|
+
nextMessageId: number;
|
|
115
|
+
/** The oldest MID still mapped. Past `MAX_MESSAGE_IDS` mapped MIDs the oldest are dropped - see
|
|
116
|
+
* `MessageTarget.assignOrGetMid`. */
|
|
117
|
+
firstMessageId: number;
|
|
118
|
+
/** This session's `RopGetPropertyIdsFromNames` mapping table (`[MS-OXCPRPT]` §2.2.12), keyed by a JSON
|
|
119
|
+
* string encoding of the `{guid, kind, lid|name}` `PropertyName` the numeric ID was assigned to - see
|
|
120
|
+
* `NamedPropertyRegistry.ts`. */
|
|
121
|
+
namedProperties: Record<string, number>;
|
|
122
|
+
/** The reverse of `namedProperties` (numeric ID -> `PropertyName` JSON key) plus a monotonic counter. */
|
|
123
|
+
namedPropertyIds: Record<number, string>;
|
|
124
|
+
nextNamedPropertyId: number;
|
|
125
|
+
constructor(other: Partial<SimpleEntity> & {
|
|
126
|
+
mailboxUid: string;
|
|
127
|
+
userUid: string;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Stores `handle` at `index` with a fresh generation. Whatever previously held that index is released first
|
|
132
|
+
* (see `releaseHandle`), so a stream or cached data belonging to the old handle never attaches to the new one.
|
|
133
|
+
*/
|
|
134
|
+
export declare function assignHandle(session: MapiSessionContext, index: number, handle: MapiObjectHandle): MapiObjectHandle;
|
|
135
|
+
/**
|
|
136
|
+
* Releases the handle at `index`: drops its cached data, and releases every write stream opened against it
|
|
137
|
+
* (a stream is meaningless once its draft message is gone, and would otherwise attach to whatever reuses the
|
|
138
|
+
* index next).
|
|
139
|
+
*/
|
|
140
|
+
export declare function releaseHandle(session: MapiSessionContext, index: number): void;
|
|
141
|
+
/** The shared-store keys (FastTransfer streams, write-stream chunks) of every handle released on `session` since the
|
|
142
|
+
* last call, for the route to delete once the session's changes are saved. */
|
|
143
|
+
export declare function takeReleasedHandleData(session: MapiSessionContext): string[];
|
|
144
|
+
/** The outcome of `MapiSessionManager.save()`. `"conflict"` means another request saved the session first;
|
|
145
|
+
* `"missing"` that it expired or was ended; `"tooBig"` that it grew past `MAX_SESSION_BYTES` and was not saved. */
|
|
146
|
+
export type SessionSaveResult = "saved" | "conflict" | "missing" | "tooBig";
|
|
147
|
+
/**
|
|
148
|
+
* The storage a `MapiSessionManager` runs on: Redis when a `cache` datastore is configured, otherwise memory.
|
|
149
|
+
*
|
|
150
|
+
* A session is two entries: its JSON (`sessionKey`) and its version (`<sessionKey>.version`). A save compares the
|
|
151
|
+
* version entry only, so Redis never has to parse the session JSON.
|
|
152
|
+
*/
|
|
153
|
+
export interface MapiSessionStore {
|
|
154
|
+
get(key: string): Promise<string | undefined>;
|
|
155
|
+
create(key: string, value: string, ttlSeconds: number): Promise<void>;
|
|
156
|
+
compareAndSet(key: string, expectedVersion: number, value: string, ttlSeconds: number): Promise<"saved" | "conflict" | "missing">;
|
|
157
|
+
delete(key: string): Promise<void>;
|
|
158
|
+
/** Atomically records `sessionId` in the user's index (ordered by last use, starting at `createdAtMs`): drops ids
|
|
159
|
+
* whose session is gone, ends the least recently used sessions until fewer than `max` remain, then adds it. Returns
|
|
160
|
+
* the ids it ended. */
|
|
161
|
+
addToUserIndex(indexKey: string, sessionId: string, createdAtMs: number, max: number, ttlSeconds: number): Promise<string[]>;
|
|
162
|
+
/** `SET NX` with a TTL: `true` when the lock was free and is now held with `token`. */
|
|
163
|
+
acquireLock(key: string, token: string, ttlMs: number): Promise<boolean>;
|
|
164
|
+
/** Releases the lock only if it is still held with `token`. */
|
|
165
|
+
releaseLock(key: string, token: string): Promise<void>;
|
|
166
|
+
/** Extends the lock to `ttlMs` from now, only while it is still held with `token`. */
|
|
167
|
+
renewLock(key: string, token: string, ttlMs: number): Promise<boolean>;
|
|
168
|
+
/** Moves `sessionId` to `score` in the user's index when it is still there. */
|
|
169
|
+
touchUserIndex(indexKey: string, sessionId: string, score: number): Promise<void>;
|
|
170
|
+
getValue(key: string): Promise<string | undefined>;
|
|
171
|
+
putValue(key: string, value: string, ttlSeconds: number): Promise<void>;
|
|
172
|
+
}
|
|
173
|
+
/** Creates a session at version 0. KEYS: session, version. ARGV: json, ttl. */
|
|
174
|
+
export declare const CREATE_SCRIPT = "\nredis.call('SETEX', KEYS[1], ARGV[2], ARGV[1])\nredis.call('SETEX', KEYS[2], ARGV[2], '0')\nreturn 1\n";
|
|
175
|
+
/** Replaces the session only when its version entry still equals the expected one, without decoding the session.
|
|
176
|
+
* KEYS: session, version. ARGV: expected version, json, ttl. Returns 1 saved, 0 on a version mismatch, -1 when gone. */
|
|
177
|
+
export declare const COMPARE_AND_SET_SCRIPT = "\nlocal current = redis.call('GET', KEYS[2])\nif not current or redis.call('EXISTS', KEYS[1]) == 0 then return -1 end\nif tonumber(current) ~= tonumber(ARGV[1]) then return 0 end\nredis.call('SETEX', KEYS[1], ARGV[3], ARGV[2])\nredis.call('SETEX', KEYS[2], ARGV[3], tostring(tonumber(ARGV[1]) + 1))\nreturn 1\n";
|
|
178
|
+
/** Adds a session to its user's sorted-set index in one step, so concurrent Connects can't each see room under the
|
|
179
|
+
* cap. KEYS: index. ARGV: session id, created-at score, max, index ttl, session key prefix, session key suffix. The
|
|
180
|
+
* session keys it checks and deletes are built from the ids, so they aren't declared in KEYS (fine on one Redis node,
|
|
181
|
+
* not on a cluster). Returns the ids it ended. */
|
|
182
|
+
export declare const ADD_TO_USER_INDEX_SCRIPT = "\nlocal function sessionKey(id) return ARGV[5] .. id .. ARGV[6] end\nfor _, id in ipairs(redis.call('ZRANGE', KEYS[1], 0, -1)) do\n if redis.call('EXISTS', sessionKey(id)) == 0 then redis.call('ZREM', KEYS[1], id) end\nend\nlocal ended = {}\nwhile redis.call('ZCARD', KEYS[1]) >= tonumber(ARGV[3]) do\n local oldest = redis.call('ZRANGE', KEYS[1], 0, 0)[1]\n redis.call('ZREM', KEYS[1], oldest)\n redis.call('DEL', sessionKey(oldest), sessionKey(oldest) .. '.version')\n table.insert(ended, oldest)\nend\nredis.call('ZADD', KEYS[1], ARGV[2], ARGV[1])\nredis.call('EXPIRE', KEYS[1], ARGV[4])\nreturn ended\n";
|
|
183
|
+
/** KEYS: lock. ARGV: token, ttl ms. Returns 1 when acquired. */
|
|
184
|
+
export declare const ACQUIRE_LOCK_SCRIPT = "\nif redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then return 1 end\nreturn 0\n";
|
|
185
|
+
/** KEYS: lock. ARGV: token. Deletes the lock only while this token still holds it. */
|
|
186
|
+
export declare const RELEASE_LOCK_SCRIPT = "\nif redis.call('GET', KEYS[1]) == ARGV[1] then redis.call('DEL', KEYS[1]) end\nreturn 1\n";
|
|
187
|
+
/** KEYS: lock. ARGV: token, ttl ms. Returns 1 when the token still held the lock and it was extended. */
|
|
188
|
+
export declare const RENEW_LOCK_SCRIPT = "\nif redis.call('GET', KEYS[1]) == ARGV[1] then redis.call('PEXPIRE', KEYS[1], ARGV[2]) return 1 end\nreturn 0\n";
|
|
189
|
+
/** KEYS: user index. ARGV: score, session id. Updates the score of a member that is still present. */
|
|
190
|
+
export declare const TOUCH_USER_INDEX_SCRIPT = "\nredis.call('ZADD', KEYS[1], 'XX', ARGV[1], ARGV[2])\nreturn 1\n";
|
|
191
|
+
/** A `MapiSessionStore` on a node-redis client. Every read goes to Redis, never a per-process copy, so each
|
|
192
|
+
* replica sees the same session. */
|
|
193
|
+
export declare class RedisMapiSessionStore implements MapiSessionStore {
|
|
194
|
+
private readonly client;
|
|
195
|
+
constructor(client: any);
|
|
196
|
+
get(key: string): Promise<string | undefined>;
|
|
197
|
+
create(key: string, value: string, ttlSeconds: number): Promise<void>;
|
|
198
|
+
compareAndSet(key: string, expectedVersion: number, value: string, ttlSeconds: number): Promise<"saved" | "conflict" | "missing">;
|
|
199
|
+
delete(key: string): Promise<void>;
|
|
200
|
+
addToUserIndex(indexKey: string, sessionId: string, createdAtMs: number, max: number, ttlSeconds: number): Promise<string[]>;
|
|
201
|
+
acquireLock(key: string, token: string, ttlMs: number): Promise<boolean>;
|
|
202
|
+
releaseLock(key: string, token: string): Promise<void>;
|
|
203
|
+
renewLock(key: string, token: string, ttlMs: number): Promise<boolean>;
|
|
204
|
+
touchUserIndex(indexKey: string, sessionId: string, score: number): Promise<void>;
|
|
205
|
+
getValue(key: string): Promise<string | undefined>;
|
|
206
|
+
putValue(key: string, value: string, ttlSeconds: number): Promise<void>;
|
|
207
|
+
}
|
|
208
|
+
/** A single-process `MapiSessionStore`. Values are stored as strings so every load hands out an independent copy,
|
|
209
|
+
* exactly like the Redis store. No method awaits between reading and writing, so each one is atomic. */
|
|
210
|
+
export declare class MemoryMapiSessionStore implements MapiSessionStore {
|
|
211
|
+
private readonly entries;
|
|
212
|
+
private readonly indexes;
|
|
213
|
+
get(key: string): Promise<string | undefined>;
|
|
214
|
+
create(key: string, value: string, ttlSeconds: number): Promise<void>;
|
|
215
|
+
compareAndSet(key: string, expectedVersion: number, value: string, ttlSeconds: number): Promise<"saved" | "conflict" | "missing">;
|
|
216
|
+
delete(key: string): Promise<void>;
|
|
217
|
+
addToUserIndex(indexKey: string, sessionId: string, createdAtMs: number, max: number): Promise<string[]>;
|
|
218
|
+
acquireLock(key: string, token: string, ttlMs: number): Promise<boolean>;
|
|
219
|
+
releaseLock(key: string, token: string): Promise<void>;
|
|
220
|
+
renewLock(key: string, token: string, ttlMs: number): Promise<boolean>;
|
|
221
|
+
touchUserIndex(indexKey: string, sessionId: string, score: number): Promise<void>;
|
|
222
|
+
getValue(key: string): Promise<string | undefined>;
|
|
223
|
+
putValue(key: string, value: string, ttlSeconds: number): Promise<void>;
|
|
224
|
+
private read;
|
|
225
|
+
private write;
|
|
226
|
+
private sweep;
|
|
227
|
+
}
|
|
228
|
+
/** The store key of a session's JSON. */
|
|
229
|
+
export declare function sessionKey(sessionId: string): string;
|
|
230
|
+
/**
|
|
231
|
+
* Stores/loads `MapiSessionContext`s, keyed by the opaque session id that also becomes the `MapiContext` cookie
|
|
232
|
+
* value.
|
|
233
|
+
*
|
|
234
|
+
* Every load reads the shared store (Redis when a `cache` datastore is configured). An earlier version used
|
|
235
|
+
* `RedisCache`, whose per-process copy served a session without checking Redis, so with several replicas one pod
|
|
236
|
+
* could run ROPs against handles another pod had already changed. Saves are compare-and-set on `version`: when
|
|
237
|
+
* two requests on the same session overlap, the second save reports `"conflict"` instead of overwriting the
|
|
238
|
+
* first request's handles. `Execute` also holds the session's lock (`acquireLock`) while it runs, so an overlapping
|
|
239
|
+
* request is turned away before it has any side effects, and keeps its last response (`storeResponse`) so a retry of
|
|
240
|
+
* the same request is answered again instead of being run twice.
|
|
241
|
+
*/
|
|
242
|
+
export declare class MapiSessionManager {
|
|
243
|
+
private redisClient?;
|
|
244
|
+
private store?;
|
|
245
|
+
/** Where handlers keep FastTransfer streams and write-stream chunks: in Redis next to the sessions when configured,
|
|
246
|
+
* so any replica can continue a transfer. */
|
|
247
|
+
handleDataStore?: HandleDataStore;
|
|
248
|
+
init(): void;
|
|
249
|
+
/** Creates a session for `userUid`, ending that user's least recently used sessions (and their stored handle data)
|
|
250
|
+
* if they already hold `MAX_SESSIONS_PER_USER`. */
|
|
251
|
+
create(mailboxUid: string, userUid: string): Promise<MapiSessionContext>;
|
|
252
|
+
/** Records that `session` was just used, so the per-user cap ends the least recently used session rather than the
|
|
253
|
+
* oldest one. */
|
|
254
|
+
touch(session: MapiSessionContext): Promise<void>;
|
|
255
|
+
/** Loads a session straight from the store. A session past `MAX_SESSION_LIFETIME_MS` is ended and treated as
|
|
256
|
+
* missing. */
|
|
257
|
+
load(sessionId: string): Promise<MapiSessionContext | undefined>;
|
|
258
|
+
/** Saves `context` only if nobody else saved it since it was loaded and it is within `MAX_SESSION_BYTES`,
|
|
259
|
+
* bumping its `version` on success. */
|
|
260
|
+
save(context: MapiSessionContext): Promise<SessionSaveResult>;
|
|
261
|
+
/** Ends a session, deleting its stored handle data too. */
|
|
262
|
+
destroy(sessionId: string): Promise<void>;
|
|
263
|
+
/** Deletes every FastTransfer stream and write-stream chunk the session stored. Best effort: they expire anyway. */
|
|
264
|
+
private deleteHandleData;
|
|
265
|
+
/** Takes the session's `Execute` lock. Returns the token to release it with, or `undefined` while another
|
|
266
|
+
* request holds it. */
|
|
267
|
+
acquireLock(sessionId: string): Promise<string | undefined>;
|
|
268
|
+
releaseLock(sessionId: string, token: string): Promise<void>;
|
|
269
|
+
/** Extends the session's lock by another `SESSION_LOCK_TTL_MS`. `false` when `token` no longer holds it. */
|
|
270
|
+
renewLock(sessionId: string, token: string): Promise<boolean>;
|
|
271
|
+
/** The response last sent on this session if it answered `requestId`; `"inProgress"` while a request with that id
|
|
272
|
+
* is still running (or ran on a pod that died less than `SESSION_LOCK_TTL_MS` ago); otherwise `undefined`. */
|
|
273
|
+
storedResponse(sessionId: string, requestId: string): Promise<Buffer | "inProgress" | undefined>;
|
|
274
|
+
/** Records that `requestId` is running on this session, for `SESSION_LOCK_TTL_MS` (renewed along with the lock). A
|
|
275
|
+
* retry of it is then answered as busy instead of running a second time. */
|
|
276
|
+
markInProgress(sessionId: string, requestId: string): Promise<void>;
|
|
277
|
+
/** Extends the in-progress marker of `requestId` by another `SESSION_LOCK_TTL_MS`, only while it is still that marker:
|
|
278
|
+
* once the request's response (or anything else) has replaced it, nothing is written. `false` when it wasn't there. */
|
|
279
|
+
renewInProgress(sessionId: string, requestId: string): Promise<boolean>;
|
|
280
|
+
/** Drops the in-progress marker of `requestId` when it is still there (the request ended without storing a
|
|
281
|
+
* response), so a retry runs. Atomic, so it never deletes a response stored meanwhile. */
|
|
282
|
+
clearInProgress(sessionId: string, requestId: string): Promise<void>;
|
|
283
|
+
/** Remembers `body` as this session's answer to `requestId`. Only the latest request is kept. */
|
|
284
|
+
storeResponse(sessionId: string, requestId: string, body: Buffer): Promise<void>;
|
|
285
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirrors `@rapidrest/service-core`'s private `ModelUtils.MAX_PATTERN_LENGTH`: a `regex(...)` query operand
|
|
3
|
+
* longer than this is rejected as a potential ReDoS pattern with `INVALID_REQUEST`. The check runs against the
|
|
4
|
+
* operand AFTER escaping, so a raw client term well under the limit can still exceed it once each regex
|
|
5
|
+
* metacharacter doubles in length.
|
|
6
|
+
*/
|
|
7
|
+
export declare const MAX_REGEX_PATTERN_LENGTH = 100;
|
|
8
|
+
/**
|
|
9
|
+
* Escapes `value` for a literal-substring `regex(...)` match, truncating the raw term (never mid-escape or
|
|
10
|
+
* mid-code-point) so the escaped result always fits `MAX_REGEX_PATTERN_LENGTH`. Matching on a prefix of an
|
|
11
|
+
* over-long search term still yields a superset of the full term's matches, which is a far better outcome than
|
|
12
|
+
* the whole command failing.
|
|
13
|
+
*/
|
|
14
|
+
export declare function boundedEscapedPattern(value: string, maxLength?: number): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { RopContext, RopHandler } from "./rop/RopHandler.js";
|
|
2
|
+
/** The most ROPs processed from one `Execute` request. A real client batches far fewer; anything past this is
|
|
3
|
+
* left unprocessed. */
|
|
4
|
+
export declare const MAX_ROPS_PER_EXECUTE = 1024;
|
|
5
|
+
/** The largest `RopsList` a response can carry: `RopSize` is a 16-bit count that includes its own 2 bytes. */
|
|
6
|
+
export declare const MAX_ROPS_LIST_BYTES: number;
|
|
7
|
+
/** `RopBufferTooSmall`'s `RopId` (`[MS-OXCROPS]` §2.2.15.1). */
|
|
8
|
+
export declare const ROP_ID_BUFFER_TOO_SMALL = 255;
|
|
9
|
+
/** `MAPI_E_CALL_FAILED`: a ROP failed while running (a database error, undecodable stored data, ...). */
|
|
10
|
+
export declare const ERROR_CALL_FAILED = 2147500037;
|
|
11
|
+
/** `MAPI_E_TOO_COMPLEX`: the ROP would have gone past this request's `ExecuteBudget`. */
|
|
12
|
+
export declare const ERROR_TOO_COMPLEX = 2147746071;
|
|
13
|
+
/** Thrown by `dispatchRops`, before any ROP runs, when the room for responses can't even hold a `RopBufferTooSmall`
|
|
14
|
+
* carrying the whole request. The route answers the `Execute` with `ecBufferTooSmall` ([MS-OXCRPC]). No ROP has run. */
|
|
15
|
+
export declare class ExecuteBufferTooSmallError extends Error {
|
|
16
|
+
constructor();
|
|
17
|
+
}
|
|
18
|
+
export interface DispatchOptions {
|
|
19
|
+
/** Stop after this many ROPs. Defaults to `MAX_ROPS_PER_EXECUTE`. */
|
|
20
|
+
maxRops?: number;
|
|
21
|
+
/** The room for response ROPs, in bytes. Defaults to `MAX_ROPS_LIST_BYTES`; the route passes what the client's
|
|
22
|
+
* `MaxRopOut` leaves after the `RopSize` field and handle table. */
|
|
23
|
+
maxOutputBytes?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Walks a decoded `RopBuffer`'s `ropsList` blob, dispatching each contained ROP - identified by its own leading
|
|
27
|
+
* `RopId` byte - to the matching registered handler in turn, collecting every response into one output buffer.
|
|
28
|
+
* Deliberately ROP-agnostic: it knows nothing about any specific ROP's own fields, only how to find the *next* one,
|
|
29
|
+
* via the handler's own advanced `reader` position once `handle()` returns - see `RopBuffer.ts`'s doc comment for
|
|
30
|
+
* why a fully generic split isn't possible for this wire format.
|
|
31
|
+
*
|
|
32
|
+
* - **Malformed requests.** An unrecognized `RopId`, or a ROP whose bytes run out or can't be sized (a
|
|
33
|
+
* `DecodeError`), throws a `DecodeError`: nothing after it can be located, so the route answers 400.
|
|
34
|
+
* - **A failing ROP.** Any other error thrown by a handler fails just that ROP: its partial output is dropped, a
|
|
35
|
+
* failure response (`RopId`, the handle index it echoes, `ReturnValue` `MAPI_E_CALL_FAILED` or, past the
|
|
36
|
+
* `ExecuteBudget`, `MAPI_E_TOO_COMPLEX`) takes its place, and the following ROPs still run. The handler has
|
|
37
|
+
* already read its whole request by the time it does work that can fail, so the next ROP is still where the
|
|
38
|
+
* reader points.
|
|
39
|
+
* - **Output space.** Room for a `RopBufferTooSmall` covering the next ROP and everything after it (3 bytes plus
|
|
40
|
+
* those request bytes) is always kept free. `context.ropOutputRemaining` tells each handler the room left *after*
|
|
41
|
+
* that reserve (computed from where the reader stands, so once a handler has read its request it excludes the
|
|
42
|
+
* handler's own bytes). A response that doesn't leave the reserve is replaced by `RopBufferTooSmall` (`SizeNeeded`
|
|
43
|
+
* plus the unprocessed request bytes, from this ROP on, for the client to resend) and processing stops; that always
|
|
44
|
+
* fits, because the reserve for this ROP was kept by the one before it. Handlers that change state by how much they
|
|
45
|
+
* return (`RopQueryRows`, `RopReadStream`, `RopFastTransferSourceGetBuffer`) size their response to the room, so they
|
|
46
|
+
* are never replaced after moving a cursor. Only when the request as a whole can't be covered by a
|
|
47
|
+
* `RopBufferTooSmall` is `ExecuteBufferTooSmallError` thrown, before anything runs, and the route answers the
|
|
48
|
+
* `Execute` with `ecBufferTooSmall` ([MS-OXCRPC]). So a failed `Execute` never follows state already changed and
|
|
49
|
+
* saved, which a retry would skip past.
|
|
50
|
+
*
|
|
51
|
+
* @author Jean-Philippe Steinmetz
|
|
52
|
+
*/
|
|
53
|
+
export declare function dispatchRops(ropsList: Buffer, handlers: Map<number, RopHandler>, context: RopContext, options?: DispatchOptions): Promise<Buffer>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BufferReader } from "./BufferCursor.js";
|
|
2
|
+
import { type RecurrenceRule } from "@rapidmx/restapi";
|
|
3
|
+
/**
|
|
4
|
+
* Encodes `rule` (plus the occurrence's own `startDate`/`endDate`, needed for phase/time-of-day derivation)
|
|
5
|
+
* into an `AppointmentRecurrencePattern` blob, ready to be written as the raw content of a `PtypBinary`
|
|
6
|
+
* `PidLidAppointmentRecur` property value.
|
|
7
|
+
*/
|
|
8
|
+
export declare function encodeAppointmentRecurrence(rule: RecurrenceRule, startDate: Date, endDate: Date): Buffer;
|
|
9
|
+
/**
|
|
10
|
+
* Decodes an `AppointmentRecurrencePattern` blob (read from `reader`'s current position) back into a
|
|
11
|
+
* `RecurrenceRule`. Throws a clear error for any pattern this pragmatic subset can't represent (see this
|
|
12
|
+
* file's own doc comment) rather than silently dropping data.
|
|
13
|
+
*/
|
|
14
|
+
export declare function decodeAppointmentRecurrence(reader: BufferReader): RecurrenceRule;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small positional cursor over a `Buffer`, shared by every MAPI codec in this package (`MapiGuid`,
|
|
3
|
+
* `PropertyValue`, `RopBuffer`, and every individual ROP handler's own request/response encoding). Unlike
|
|
4
|
+
* WBXML's self-describing tag stream, MAPI's wire structures are fixed/variable-length binary records with no
|
|
5
|
+
* generic "read until end tag" mechanism, so every codec needs the same little-endian primitive reads/writes
|
|
6
|
+
* with automatic offset advancement - reimplementing that per file would be pure repetition across the dozens
|
|
7
|
+
* of ROP handlers this phase's plan calls for, unlike EAS's WBXML codec, which only needed one such reader.
|
|
8
|
+
*
|
|
9
|
+
* Underlying `Buffer` methods already bounds-check and throw `RangeError` on overflow/truncation for every
|
|
10
|
+
* fixed-width primitive read - a malformed or truncated ROP buffer legitimately failing loudly is the correct
|
|
11
|
+
* behavior, not something to swallow here. `readBytes`/`readNullTerminatedUtf16LE`/`readNullTerminatedString8`
|
|
12
|
+
* are the three exceptions: `Buffer.subarray` silently clamps instead of throwing, and a null-terminated scan
|
|
13
|
+
* has no fixed width to bounds-check against `Buffer` methods at all, so those three enforce the same
|
|
14
|
+
* "malformed/truncated input throws" contract explicitly below instead of relying on it happening for free.
|
|
15
|
+
*
|
|
16
|
+
* @author Jean-Philippe Steinmetz
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* A request that can't be decoded: truncated, or carrying a value (such as an unknown property type) whose wire
|
|
20
|
+
* size can't be determined. `RopDispatcher` answers the whole `Execute` with a 400 for one of these, since the bytes
|
|
21
|
+
* of every later ROP can no longer be located.
|
|
22
|
+
*/
|
|
23
|
+
export declare class DecodeError extends RangeError {
|
|
24
|
+
constructor(message: string);
|
|
25
|
+
}
|
|
26
|
+
export declare class BufferReader {
|
|
27
|
+
private readonly buffer;
|
|
28
|
+
private offset;
|
|
29
|
+
constructor(buffer: Buffer, offset?: number);
|
|
30
|
+
get position(): number;
|
|
31
|
+
get remaining(): number;
|
|
32
|
+
hasMore(): boolean;
|
|
33
|
+
/** Moves to `position` (backwards or forwards). Throws a `RangeError` for a position outside the buffer. */
|
|
34
|
+
seek(position: number): void;
|
|
35
|
+
readUInt8(): number;
|
|
36
|
+
readInt16LE(): number;
|
|
37
|
+
readUInt16LE(): number;
|
|
38
|
+
readInt32LE(): number;
|
|
39
|
+
readUInt32LE(): number;
|
|
40
|
+
readFloatLE(): number;
|
|
41
|
+
readDoubleLE(): number;
|
|
42
|
+
readBigInt64LE(): bigint;
|
|
43
|
+
readBigUInt64LE(): bigint;
|
|
44
|
+
/** Unlike every fixed-width `readXxxLE` above, `Buffer.prototype.subarray` never throws - an out-of-range
|
|
45
|
+
* `length` (negative, or reaching past the buffer's end) silently clamps instead, which would otherwise let
|
|
46
|
+
* a malformed/truncated ROP buffer produce garbage-but-not-erroring reads (a negative `length` even rewinds
|
|
47
|
+
* `offset` backwards) instead of failing loudly the way this class's own doc comment promises. Explicitly
|
|
48
|
+
* bounds-checked here for that reason. */
|
|
49
|
+
readBytes(length: number): Buffer;
|
|
50
|
+
/** Reads a UTF-16LE string up to (and consuming) its terminating `0x0000` code unit - `PtypString`'s wire
|
|
51
|
+
* encoding. Scans on 2-byte boundaries, since a null code unit's low/high byte pair can't be mistaken for
|
|
52
|
+
* one half of a non-null UTF-16 code unit at an odd offset the way a naive single-byte scan could.
|
|
53
|
+
*
|
|
54
|
+
* Throws if no terminator is found before running out of buffer, rather than silently returning whatever
|
|
55
|
+
* content preceded the truncation and leaving `offset` past the buffer's actual end - a truncated/malformed
|
|
56
|
+
* string is exactly the kind of input this class's own doc comment says should fail loudly. This also
|
|
57
|
+
* closes a real DoS: `PropertyValue.ts`'s `readCountedArray` loops a client-controlled element count with
|
|
58
|
+
* no independent cap, trusting each element's own reader to terminate the loop early against a too-small
|
|
59
|
+
* buffer the way every fixed-width reader already does - without this, `PtypMultipleString`'s
|
|
60
|
+
* `readNullTerminatedUtf16LE` calls kept silently returning `""` past the buffer's end forever instead. */
|
|
61
|
+
readNullTerminatedUtf16LE(): string;
|
|
62
|
+
/** Reads a single-byte-terminated 8-bit string (`PtypString8`'s wire encoding). Multibyte string content
|
|
63
|
+
* itself is treated as UTF-8, a pragmatic choice documented in `PropertyValue.ts` rather than the
|
|
64
|
+
* "externally specified encoding" the spec leaves open-ended.
|
|
65
|
+
*
|
|
66
|
+
* Throws if no terminator is found before running out of buffer - see `readNullTerminatedUtf16LE`'s own
|
|
67
|
+
* doc comment for why (identical reasoning, including the `readCountedArray` DoS this closes). */
|
|
68
|
+
readNullTerminatedString8(): string;
|
|
69
|
+
}
|
|
70
|
+
/** The write-side counterpart to `BufferReader`, accumulating chunks and concatenating once via `toBuffer()` -
|
|
71
|
+
* the same "array of pieces, `Buffer.concat` once at the end" idiom `WbxmlEncoder` already uses for its own
|
|
72
|
+
* byte-array accumulator, adapted for multi-byte little-endian fields instead of single bytes. */
|
|
73
|
+
export declare class BufferWriter {
|
|
74
|
+
private readonly chunks;
|
|
75
|
+
private byteLength;
|
|
76
|
+
/** The number of bytes written so far. */
|
|
77
|
+
get length(): number;
|
|
78
|
+
private push;
|
|
79
|
+
writeUInt8(value: number): this;
|
|
80
|
+
writeInt16LE(value: number): this;
|
|
81
|
+
writeUInt16LE(value: number): this;
|
|
82
|
+
writeInt32LE(value: number): this;
|
|
83
|
+
writeUInt32LE(value: number): this;
|
|
84
|
+
writeFloatLE(value: number): this;
|
|
85
|
+
writeDoubleLE(value: number): this;
|
|
86
|
+
writeBigInt64LE(value: bigint): this;
|
|
87
|
+
writeBigUInt64LE(value: bigint): this;
|
|
88
|
+
writeBytes(value: Buffer): this;
|
|
89
|
+
writeNullTerminatedUtf16LE(value: string): this;
|
|
90
|
+
writeNullTerminatedString8(value: string): this;
|
|
91
|
+
toBuffer(): Buffer;
|
|
92
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BufferReader } from "./BufferCursor.js";
|
|
2
|
+
/** Encodes `icalUid` into a `GlobalObjectId` BLOB in the `VCALID` form a real client synthesizes from a plain
|
|
3
|
+
* iCalendar UID (see this file's own doc comment for why that's the form that matters here), using `at` as the
|
|
4
|
+
* `CreationTime`. */
|
|
5
|
+
export declare function encodeGlobalObjectId(icalUid: string, at: Date): Buffer;
|
|
6
|
+
/** Decodes a `GlobalObjectId` BLOB (read from `reader`'s current position) back into the `icalUid` it stands for.
|
|
7
|
+
* A `VCALID` (see this file's own doc comment) yields the UID it wraps. Anything else is a native `OutlookID`,
|
|
8
|
+
* whose `Data` is binary, not text: per `[MS-OXCICAL]`'s UID conversion, the iCalendar UID of such an object is the
|
|
9
|
+
* uppercase hex of the whole `GlobalObjectId` with the instance date (`YH`/`YL`/`M`/`D`, bytes 16-19) zeroed, so
|
|
10
|
+
* that is what is returned (compare it case-insensitively). Throws if `ByteArrayID` doesn't match the spec's own
|
|
11
|
+
* fixed constant - a real, spec-mandated identity check, not an invented restriction. */
|
|
12
|
+
export declare function decodeGlobalObjectId(reader: BufferReader): string;
|
|
13
|
+
/** The `YYYY-MM-DD` occurrence date a `GlobalObjectId` names (`YH`/`YL`/`M`/`D`), or `undefined` when it names the
|
|
14
|
+
* whole series (a zero year). `globalObjectId` must already have decoded successfully. */
|
|
15
|
+
export declare function globalObjectIdInstanceDate(globalObjectId: Buffer): string | undefined;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { BufferReader } from "./BufferCursor.js";
|
|
2
|
+
/** Encodes a standard hyphenated GUID/UUID string into its 16-byte MS wire form. */
|
|
3
|
+
export declare function encodeGuid(guid: string): Buffer;
|
|
4
|
+
/** Decodes a 16-byte MS wire-form GUID (read from `reader`'s current position) into a standard hyphenated
|
|
5
|
+
* GUID/UUID string. */
|
|
6
|
+
export declare function decodeGuid(reader: BufferReader): string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { BufferReader } from "./BufferCursor.js";
|
|
2
|
+
/** Encodes `timezone` (an IANA identifier, or `"UTC"`) into a 48-byte `TimeZoneStruct` BLOB, using `at` as the
|
|
3
|
+
* reference instant for resolving a non-UTC zone's current fixed offset. */
|
|
4
|
+
export declare function encodeTimeZoneStruct(timezone: string, at: Date): Buffer;
|
|
5
|
+
/** Decodes a `TimeZoneStruct` BLOB (read from `reader`'s current position) into an approximate IANA zone
|
|
6
|
+
* identifier - see this file's own doc comment for the lossy `Etc/GMT±N` fallback's exact rounding rules. */
|
|
7
|
+
export declare function decodeTimeZoneStruct(reader: BufferReader): string;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { BufferReader, BufferWriter } from "./BufferCursor.js";
|
|
2
|
+
/**
|
|
3
|
+
* The property-value system every meaningful ROP depends on (`RopGetPropertiesSpecific`/`RopSetProperties`,
|
|
4
|
+
* table row data in `RopQueryRows`, ...), per `[MS-OXCDATA]` §2.11. Only the property types this phase's
|
|
5
|
+
* planned ROP handlers actually need are implemented (`readPropertyValue`/`writePropertyValue` throw a clear
|
|
6
|
+
* error for anything else) - add more as a real handler needs them, not speculatively.
|
|
7
|
+
*
|
|
8
|
+
* Numeric values and member names (`Ptyp*`) are exactly as published in `[MS-OXCDATA]`'s "Property Data
|
|
9
|
+
* Types" table - the full `Ptyp`-prefixed spec names are used rather than shorter aliases (`String`,
|
|
10
|
+
* `Boolean`, ...) both for fidelity to the spec and because those shorter names collide with this project's
|
|
11
|
+
* lint config's `id-denylist` rule (which exists to stop primitive-wrapper-like names shadowing globals).
|
|
12
|
+
*/
|
|
13
|
+
export declare enum PropertyType {
|
|
14
|
+
PtypNull = 1,
|
|
15
|
+
PtypInteger16 = 2,
|
|
16
|
+
PtypInteger32 = 3,
|
|
17
|
+
PtypFloating32 = 4,
|
|
18
|
+
PtypFloating64 = 5,
|
|
19
|
+
PtypBoolean = 11,
|
|
20
|
+
PtypInteger64 = 20,
|
|
21
|
+
PtypString8 = 30,
|
|
22
|
+
PtypString = 31,
|
|
23
|
+
PtypTime = 64,
|
|
24
|
+
PtypGuid = 72,
|
|
25
|
+
PtypBinary = 258,
|
|
26
|
+
PtypMultipleInteger32 = 4099,
|
|
27
|
+
PtypMultipleString8 = 4126,
|
|
28
|
+
PtypMultipleString = 4127,
|
|
29
|
+
PtypMultipleBinary = 4354
|
|
30
|
+
}
|
|
31
|
+
export interface PropertyTag {
|
|
32
|
+
propertyId: number;
|
|
33
|
+
propertyType: PropertyType;
|
|
34
|
+
}
|
|
35
|
+
export interface TypedPropertyValue {
|
|
36
|
+
propertyType: PropertyType;
|
|
37
|
+
value: PropertyValueData;
|
|
38
|
+
}
|
|
39
|
+
export interface TaggedPropertyValue {
|
|
40
|
+
propertyId: number;
|
|
41
|
+
propertyType: PropertyType;
|
|
42
|
+
value: PropertyValueData;
|
|
43
|
+
}
|
|
44
|
+
export type PropertyValueData = number | bigint | boolean | string | Buffer | Date | string[] | number[] | Buffer[];
|
|
45
|
+
/**
|
|
46
|
+
* `PropertyTag` structure (`[MS-OXCDATA]` §2.9): a 4-byte value with **`PropertyType` in the low-order 16
|
|
47
|
+
* bits and `PropertyId` in the high-order 16 bits** - confirmed against the spec's own bit-range description
|
|
48
|
+
* and cross-checked against the well-known real property-tag constant `PR_SUBJECT = 0x0037001F` (`PidTagSubject`
|
|
49
|
+
* = property ID `0x0037`, `PtypString` = type `0x001F`), which only decodes correctly with `PropertyType` as
|
|
50
|
+
* the low half. On the wire (little-endian), that means `PropertyType` is read/written first.
|
|
51
|
+
*/
|
|
52
|
+
export declare function readPropertyTag(reader: BufferReader): PropertyTag;
|
|
53
|
+
/** The most `PropertyTag`s accepted in one request's tag array (`RopSetColumns`, `RopGetPropertiesSpecific`,
|
|
54
|
+
* the FastTransfer source ROPs, NSPI `Columns`). Every tag multiplies the per-row resolution work and, for
|
|
55
|
+
* `RopSetColumns`, the stored session state; a real client asks for far fewer. */
|
|
56
|
+
export declare const MAX_PROPERTY_TAG_COUNT = 256;
|
|
57
|
+
/**
|
|
58
|
+
* Reads `count` consecutive `PropertyTag`s. When `count` exceeds `MAX_PROPERTY_TAG_COUNT`, the tags' bytes are
|
|
59
|
+
* skipped (still bounds-checked, so a count larger than the buffer throws) and `undefined` is returned, leaving
|
|
60
|
+
* the reader positioned after the array so the caller can send an error response and keep parsing later ROPs.
|
|
61
|
+
*/
|
|
62
|
+
export declare function readPropertyTagArray(reader: BufferReader, count: number): PropertyTag[] | undefined;
|
|
63
|
+
export declare function writePropertyTag(writer: BufferWriter, tag: PropertyTag): void;
|
|
64
|
+
/** `TypedPropertyValue` structure (`[MS-OXCDATA]` §2.11.4): `PropertyType` (2 bytes) followed by the value
|
|
65
|
+
* itself, encoded per `readPropertyValue`/`writePropertyValue` below. */
|
|
66
|
+
export declare function readTypedPropertyValue(reader: BufferReader): TypedPropertyValue;
|
|
67
|
+
export declare function writeTypedPropertyValue(writer: BufferWriter, typed: TypedPropertyValue): void;
|
|
68
|
+
/** `TaggedPropertyValue` structure (`[MS-OXCDATA]` §2.11.4, confirmed via `RopSetProperties`'s own request-buffer
|
|
69
|
+
* page): a full `PropertyTag` (4 bytes - `PropertyId` **and** `PropertyType`, unlike `TypedPropertyValue`'s bare
|
|
70
|
+
* `PropertyType`) followed by the value itself. Used where a property's identity can't be inferred from
|
|
71
|
+
* context (e.g. `RopSetProperties`, which sets an arbitrary, client-chosen set of properties in one call). */
|
|
72
|
+
export declare function readTaggedPropertyValue(reader: BufferReader): TaggedPropertyValue;
|
|
73
|
+
export declare function writeTaggedPropertyValue(writer: BufferWriter, tagged: TaggedPropertyValue): void;
|
|
74
|
+
/**
|
|
75
|
+
* Reads a bare `PropertyValue` (`[MS-OXCDATA]` §2.11.2) of the given, already-known `type`. `PtypBinary`'s
|
|
76
|
+
* length prefix is 16 bits and every `PtypMultiple*`'s value count is 32 bits wide, per the spec's explicit
|
|
77
|
+
* "in the context of ROP buffers" sizing rule - the only context every planned caller of this function
|
|
78
|
+
* operates in (the wider 32-bit `PtypBinary` count only applies to extended rules / a specific MAPI/HTTP
|
|
79
|
+
* structure this library doesn't implement).
|
|
80
|
+
*/
|
|
81
|
+
export declare function readPropertyValue(reader: BufferReader, type: PropertyType): PropertyValueData;
|
|
82
|
+
export declare function writePropertyValue(writer: BufferWriter, type: PropertyType, value: PropertyValueData): void;
|
|
83
|
+
/** Converts a `PtypTime` value (100-ns intervals since 1601-01-01) into a JS `Date`. Sub-millisecond
|
|
84
|
+
* precision is truncated - `Date` has no finer resolution, a real, bounded, documented gap. */
|
|
85
|
+
export declare function filetimeToDate(filetime: bigint): Date;
|
|
86
|
+
/** Converts a JS `Date` into a `PtypTime` value (100-ns intervals since 1601-01-01). */
|
|
87
|
+
export declare function dateToFiletime(date: Date): bigint;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The outer `ROP input/output buffer` framing (`[MS-OXCROPS]` §2.2.1): `RopSize` (2 bytes, the size of
|
|
3
|
+
* itself plus `RopsList`) + `RopsList` (variable) + `ServerObjectHandleTable` (the remaining bytes, each a
|
|
4
|
+
* 32-bit Server object handle referenced by index from within the ROPs).
|
|
5
|
+
*
|
|
6
|
+
* Deliberately **ROP-agnostic**: `RopsList` is a concatenated sequence of individual ROP request/response
|
|
7
|
+
* buffers with no per-ROP length prefix (unlike WBXML's tag-based self-description) - each ROP's own byte
|
|
8
|
+
* layout is bespoke and keyed by its leading `RopId` byte, so splitting `RopsList` into individual ROPs
|
|
9
|
+
* requires ROP-specific decode logic this generic framing codec can't provide. A `RopDispatcher` (a later
|
|
10
|
+
* build step, once real `RopHandler`s exist) walks `ropsList` with a `BufferReader`, reading each ROP's
|
|
11
|
+
* `RopId` and delegating to the matching handler, which itself knows how many bytes its own ROP consumes -
|
|
12
|
+
* exactly the same "handler owns its own wire format" division of responsibility `EasCommandHandler` already
|
|
13
|
+
* uses for EAS commands.
|
|
14
|
+
*
|
|
15
|
+
* @author Jean-Philippe Steinmetz
|
|
16
|
+
*/
|
|
17
|
+
export interface RopBuffer {
|
|
18
|
+
/** Raw bytes of the concatenated ROP request/response entries (`RopsList`). */
|
|
19
|
+
ropsList: Buffer;
|
|
20
|
+
/** The `ServerObjectHandleTable` array - one 32-bit handle per referenced Server object, in the order
|
|
21
|
+
* ROPs within `ropsList` reference them by index. */
|
|
22
|
+
handleTable: number[];
|
|
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;
|
|
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. */
|
|
32
|
+
export declare function decodeRopBuffer(buffer: Buffer): RopBuffer;
|