@rapidmx/mapi-plugin 1.0.0-beta.3
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 +297 -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 +119 -0
- package/dist/lib/MapiSessionManager.js.map +1 -0
- package/dist/lib/RopDispatcher.js +34 -0
- package/dist/lib/RopDispatcher.js.map +1 -0
- package/dist/lib/codec/AppointmentRecurrence.js +314 -0
- package/dist/lib/codec/AppointmentRecurrence.js.map +1 -0
- package/dist/lib/codec/BufferCursor.js +213 -0
- package/dist/lib/codec/BufferCursor.js.map +1 -0
- package/dist/lib/codec/GlobalObjectId.js +92 -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 +93 -0
- package/dist/lib/codec/MapiTimeZone.js.map +1 -0
- package/dist/lib/codec/PropertyValue.js +214 -0
- package/dist/lib/codec/PropertyValue.js.map +1 -0
- package/dist/lib/codec/RopBuffer.js +26 -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 +84 -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/CalendarEventTarget.js +27 -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 +23 -0
- package/dist/lib/rop/ContactTarget.js.map +1 -0
- package/dist/lib/rop/FastTransferStream.js +98 -0
- package/dist/lib/rop/FastTransferStream.js.map +1 -0
- package/dist/lib/rop/FolderTarget.js +111 -0
- package/dist/lib/rop/FolderTarget.js.map +1 -0
- package/dist/lib/rop/MeetingMessageClassHandler.js +95 -0
- package/dist/lib/rop/MeetingMessageClassHandler.js.map +1 -0
- package/dist/lib/rop/MessageBodyStream.js +42 -0
- package/dist/lib/rop/MessageBodyStream.js.map +1 -0
- package/dist/lib/rop/MessageTarget.js +44 -0
- package/dist/lib/rop/MessageTarget.js.map +1 -0
- package/dist/lib/rop/NamedPropertyRegistry.js +66 -0
- package/dist/lib/rop/NamedPropertyRegistry.js.map +1 -0
- package/dist/lib/rop/PropertyResolvers.js +313 -0
- package/dist/lib/rop/PropertyResolvers.js.map +1 -0
- package/dist/lib/rop/RopCreateMessageHandler.js +53 -0
- package/dist/lib/rop/RopCreateMessageHandler.js.map +1 -0
- package/dist/lib/rop/RopDeleteFolderHandler.js +133 -0
- package/dist/lib/rop/RopDeleteFolderHandler.js.map +1 -0
- package/dist/lib/rop/RopDeleteMessagesHandler.js +70 -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 +61 -0
- package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js.map +1 -0
- package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js +75 -0
- package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js.map +1 -0
- package/dist/lib/rop/RopGetContentsTableHandler.js +76 -0
- package/dist/lib/rop/RopGetContentsTableHandler.js.map +1 -0
- package/dist/lib/rop/RopGetHierarchyTableHandler.js +40 -0
- package/dist/lib/rop/RopGetHierarchyTableHandler.js.map +1 -0
- package/dist/lib/rop/RopGetPropertiesSpecificHandler.js +55 -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 +2 -0
- package/dist/lib/rop/RopHandler.js.map +1 -0
- package/dist/lib/rop/RopLogonHandler.js +132 -0
- package/dist/lib/rop/RopLogonHandler.js.map +1 -0
- package/dist/lib/rop/RopOpenFolderHandler.js +46 -0
- package/dist/lib/rop/RopOpenFolderHandler.js.map +1 -0
- package/dist/lib/rop/RopOpenMessageHandler.js +81 -0
- package/dist/lib/rop/RopOpenMessageHandler.js.map +1 -0
- package/dist/lib/rop/RopOpenStreamHandler.js +81 -0
- package/dist/lib/rop/RopOpenStreamHandler.js.map +1 -0
- package/dist/lib/rop/RopQueryRowsHandler.js +91 -0
- package/dist/lib/rop/RopQueryRowsHandler.js.map +1 -0
- package/dist/lib/rop/RopReadStreamHandler.js +55 -0
- package/dist/lib/rop/RopReadStreamHandler.js.map +1 -0
- package/dist/lib/rop/RopReleaseHandler.js +20 -0
- package/dist/lib/rop/RopReleaseHandler.js.map +1 -0
- package/dist/lib/rop/RopSaveChangesMessageHandler.js +210 -0
- package/dist/lib/rop/RopSaveChangesMessageHandler.js.map +1 -0
- package/dist/lib/rop/RopSetColumnsHandler.js +52 -0
- package/dist/lib/rop/RopSetColumnsHandler.js.map +1 -0
- package/dist/lib/rop/RopSetPropertiesHandler.js +162 -0
- package/dist/lib/rop/RopSetPropertiesHandler.js.map +1 -0
- package/dist/lib/rop/RopSubmitMessageHandler.js +314 -0
- package/dist/lib/rop/RopSubmitMessageHandler.js.map +1 -0
- package/dist/lib/rop/RopWriteStreamHandler.js +45 -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 +19 -0
- package/dist/lib/rop/TaskTarget.js.map +1 -0
- package/dist/lib/sql/MapiEmsmdbRouteSQL.js +84 -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 +82 -0
- package/dist/types/BaseMapiNspiRoute.d.ts +38 -0
- package/dist/types/MapiSessionManager.d.ts +130 -0
- package/dist/types/RopDispatcher.d.ts +17 -0
- package/dist/types/codec/AppointmentRecurrence.d.ts +14 -0
- package/dist/types/codec/BufferCursor.d.ts +78 -0
- package/dist/types/codec/GlobalObjectId.d.ts +11 -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 +77 -0
- package/dist/types/codec/RopBuffer.d.ts +25 -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 +19 -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 +24 -0
- package/dist/types/rop/CalendarEventTarget.d.ts +29 -0
- package/dist/types/rop/CalendarNamedProperties.d.ts +37 -0
- package/dist/types/rop/ContactTarget.d.ts +29 -0
- package/dist/types/rop/FastTransferStream.d.ts +18 -0
- package/dist/types/rop/FolderTarget.d.ts +63 -0
- package/dist/types/rop/MeetingMessageClassHandler.d.ts +36 -0
- package/dist/types/rop/MessageBodyStream.d.ts +26 -0
- package/dist/types/rop/MessageTarget.d.ts +38 -0
- package/dist/types/rop/NamedPropertyRegistry.d.ts +46 -0
- package/dist/types/rop/PropertyResolvers.d.ts +60 -0
- package/dist/types/rop/RopCreateMessageHandler.d.ts +23 -0
- package/dist/types/rop/RopDeleteFolderHandler.d.ts +38 -0
- package/dist/types/rop/RopDeleteMessagesHandler.d.ts +31 -0
- package/dist/types/rop/RopFastTransferSourceCopyPropertiesHandler.d.ts +24 -0
- package/dist/types/rop/RopFastTransferSourceCopyToHandler.d.ts +24 -0
- package/dist/types/rop/RopFastTransferSourceGetBufferHandler.d.ts +23 -0
- package/dist/types/rop/RopGetContentsTableHandler.d.ts +31 -0
- package/dist/types/rop/RopGetHierarchyTableHandler.d.ts +17 -0
- package/dist/types/rop/RopGetPropertiesSpecificHandler.d.ts +23 -0
- package/dist/types/rop/RopGetPropertyIdsFromNamesHandler.d.ts +23 -0
- package/dist/types/rop/RopHandler.d.ts +67 -0
- package/dist/types/rop/RopLogonHandler.d.ts +31 -0
- package/dist/types/rop/RopOpenFolderHandler.d.ts +19 -0
- package/dist/types/rop/RopOpenMessageHandler.d.ts +27 -0
- package/dist/types/rop/RopOpenStreamHandler.d.ts +27 -0
- package/dist/types/rop/RopQueryRowsHandler.d.ts +36 -0
- package/dist/types/rop/RopReadStreamHandler.d.ts +18 -0
- package/dist/types/rop/RopReleaseHandler.d.ts +14 -0
- package/dist/types/rop/RopSaveChangesMessageHandler.d.ts +44 -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 +70 -0
- package/dist/types/rop/RopWriteStreamHandler.d.ts +21 -0
- package/dist/types/rop/TaskNamedProperties.d.ts +24 -0
- package/dist/types/rop/TaskTarget.d.ts +24 -0
- package/dist/types/sql/MapiEmsmdbRouteSQL.d.ts +19 -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,82 @@
|
|
|
1
|
+
import { type JWTUser } from "@rapidrest/core";
|
|
2
|
+
import { HttpRequest, HttpResponse } from "@rapidrest/service-core";
|
|
3
|
+
import { Mailbox } from "@rapidmx/restapi";
|
|
4
|
+
/**
|
|
5
|
+
* Abstract base for the single fixed EMSMDB endpoint (`POST /mapi/emsmdb` by `[MS-OXCMAPIHTTP]` convention,
|
|
6
|
+
* though the concrete path is left to the consuming application to mount via `@Route(...)` - see
|
|
7
|
+
* `BaseEasRoute.ts` for the identical undecorated-base-class pattern this follows). Like EAS, MAPI/HTTP
|
|
8
|
+
* multiplexes several request types against one URL - here via the `X-RequestType` **header**
|
|
9
|
+
* (`Connect`/`Execute`/`Disconnect`/`NotificationWait`) rather than a query parameter - so there is exactly
|
|
10
|
+
* one `@Post()` method, not one per request type.
|
|
11
|
+
*
|
|
12
|
+
* **Auth**: `@Auth(["jwt"])` - unchanged from every other route in this app. Real, modern Exchange Server's
|
|
13
|
+
* own MAPI virtual directory genuinely supports `OAuth` as a configured `IISAuthenticationMethods` value
|
|
14
|
+
* alongside `NTLM`/`Negotiate`, so Bearer-token auth on this exact endpoint is real current Exchange
|
|
15
|
+
* behavior, not a deviation this library invents - see the architecture plan's "Auth" section.
|
|
16
|
+
*
|
|
17
|
+
* **Connect/Disconnect are complete**: `Connect` establishes a real `MapiSessionContext` (via
|
|
18
|
+
* `MapiSessionManager`) and sets the two spec-fixed session cookies (`MapiContext`/`MapiSequence`);
|
|
19
|
+
* `Disconnect` releases the session. **`Execute` dispatches real ROPs** (starting with `RopLogon`/`RopRelease`
|
|
20
|
+
* - build-order step 4) via `RopDispatcher`, using whichever `RopHandler`s `ropHandlerClasses` registers;
|
|
21
|
+
* an unrecognized `RopId` simply stops processing (see `RopDispatcher`'s own doc comment for why), the same
|
|
22
|
+
* "real, functional, no commands implemented yet" stance `BaseEasRoute`'s own skeleton step took for its
|
|
23
|
+
* empty `commandHandlerClasses` before `ProvisionCommand` landed - here scoped per-ROP instead of per-request.
|
|
24
|
+
*
|
|
25
|
+
* **Always non-chunked**: every response here uses `Content-Length` (via `res.status(200).send(buffer)`),
|
|
26
|
+
* never `Transfer-Encoding: chunked` - both are equally spec-valid per `[MS-OXCMAPIHTTP]`'s own "Common
|
|
27
|
+
* Response Format", and skipping the `PROCESSING`/`PENDING`/`DONE` keep-alive meta-tag streaming matches
|
|
28
|
+
* every other route in this library's own response idiom. See the architecture plan's "Protocol facts"
|
|
29
|
+
* section for the full reasoning and its documented limitation.
|
|
30
|
+
*
|
|
31
|
+
* `mailboxClass`/`folderClass` are supplied by the Mongo/SQL concrete subclasses, following the exact
|
|
32
|
+
* one-line-per-backend pattern used throughout this library's other routes/jobs. `folderRepo` is built once
|
|
33
|
+
* here (not per-handler, unlike EAS's per-command repo pattern) and threaded through `RopContext` to every
|
|
34
|
+
* `RopHandler` - see `RopHandler.ts`'s own doc comment for why.
|
|
35
|
+
*
|
|
36
|
+
* @author Jean-Philippe Steinmetz
|
|
37
|
+
*/
|
|
38
|
+
export declare abstract class BaseMapiEmsmdbRoute<M extends Mailbox> {
|
|
39
|
+
protected abstract mailboxClass: any;
|
|
40
|
+
protected abstract folderClass: any;
|
|
41
|
+
protected abstract messageClass: any;
|
|
42
|
+
protected abstract calendarEventClass: any;
|
|
43
|
+
protected abstract contactClass: any;
|
|
44
|
+
protected abstract taskClass: any;
|
|
45
|
+
protected abstract labelClass: any;
|
|
46
|
+
/** ROP handler classes to instantiate (one each) in `@Init`, keyed by their own `ropId`. Empty until a
|
|
47
|
+
* concrete `RopHandler` lands - every ROP is then simply left unprocessed (see `RopDispatcher`'s own doc
|
|
48
|
+
* comment), the correct, honest behavior for a transport skeleton with no ROPs implemented yet. */
|
|
49
|
+
protected ropHandlerClasses: any[];
|
|
50
|
+
private _objectFactory?;
|
|
51
|
+
private mailboxRepo?;
|
|
52
|
+
private folderRepo?;
|
|
53
|
+
private messageRepo?;
|
|
54
|
+
private calendarEventRepo?;
|
|
55
|
+
private contactRepo?;
|
|
56
|
+
private taskRepo?;
|
|
57
|
+
private labelRepo?;
|
|
58
|
+
private sessionManager?;
|
|
59
|
+
private readonly ropHandlers;
|
|
60
|
+
private blobStore?;
|
|
61
|
+
private scanPipeline?;
|
|
62
|
+
private mailTransport?;
|
|
63
|
+
private logger;
|
|
64
|
+
init(): Promise<void>;
|
|
65
|
+
dispatch(req: HttpRequest, res: HttpResponse, user?: JWTUser): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* `Connect` establishes a new Session Context - never trusts the request body's own `UserDn` field for
|
|
68
|
+
* mailbox identity (the same "never a client-supplied mailbox, always resolved from the authenticated
|
|
69
|
+
* JWT" principle `BaseEasRoute`/`BaseSearchRoute` already apply via `resolveCallerMailboxUid`) - so the
|
|
70
|
+
* request body is never decoded at all here.
|
|
71
|
+
*/
|
|
72
|
+
private handleConnect;
|
|
73
|
+
/**
|
|
74
|
+
* `Execute` decodes the outer envelope (`Flags`/`RopBufferSize`/`RopBuffer`/...) and the inner ROP buffer
|
|
75
|
+
* framing (`RopBuffer.ts`), dispatches every contained ROP via `RopDispatcher`, and re-encodes the
|
|
76
|
+
* collected responses - preserving the incoming `handleTable` unchanged (this pragmatic subset never
|
|
77
|
+
* allocates/frees table-wide handle slots at the framing level; individual `RopHandler`s manage their own
|
|
78
|
+
* entries within `session.handles` instead).
|
|
79
|
+
*/
|
|
80
|
+
private handleExecute;
|
|
81
|
+
private handleDisconnect;
|
|
82
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type JWTUser } from "@rapidrest/core";
|
|
2
|
+
import { HttpRequest, HttpResponse } from "@rapidrest/service-core";
|
|
3
|
+
import { Mailbox } from "@rapidmx/restapi";
|
|
4
|
+
/**
|
|
5
|
+
* Abstract base for the single fixed NSPI endpoint (`POST /mapi/nspi` by `[MS-OXCMAPIHTTP]` convention, though
|
|
6
|
+
* the concrete path is left to the consuming application to mount via `@Route(...)`, the identical
|
|
7
|
+
* undecorated-base-class pattern `BaseMapiEmsmdbRoute`/`BaseEasRoute` already establish). Dispatches on the
|
|
8
|
+
* `X-RequestType` header, the same multiplexing mechanism EMSMDB uses - but unlike EMSMDB, there is no ROP
|
|
9
|
+
* buffer framing here: each `X-RequestType` value (`Bind`/`Unbind`/`GetMatches`) is its own flat, self-
|
|
10
|
+
* contained request/response body (`[MS-OXCMAPIHTTP]` §2.2.5), so this route dispatches directly to a small
|
|
11
|
+
* handler function per operation instead of decoding a multiplexed stream.
|
|
12
|
+
*
|
|
13
|
+
* **Pragmatic subset scope**: only `Bind`/`Unbind`/`GetMatches` are implemented - real NSPI's other dozen-plus
|
|
14
|
+
* request types (`QueryRows`, `ResolveNames`, `GetProps`, `ModProps`, ...) are genuine full address-book/
|
|
15
|
+
* directory-replication operations this deployment's own minimal GAL-lookup use case doesn't need; an
|
|
16
|
+
* unrecognized `X-RequestType` gets a `501`, the same "recognized-but-deferred vs malformed" distinction
|
|
17
|
+
* `BaseMapiEmsmdbRoute`'s own `NotificationWait` case already draws. See `NspiBindHandler.ts`'s own doc comment
|
|
18
|
+
* for why no real NSPI session state is created or tracked between calls.
|
|
19
|
+
*
|
|
20
|
+
* **Auth**: `@Auth(["jwt"])`, unchanged from every other route in this app - the same reasoning
|
|
21
|
+
* `BaseMapiEmsmdbRoute`'s own doc comment already gives for why Bearer-token auth on an address-book endpoint
|
|
22
|
+
* is real, current Exchange behavior, not a deviation this library invents.
|
|
23
|
+
*
|
|
24
|
+
* `mailboxClass`/`contactClass` are supplied by the Mongo/SQL concrete subclasses. GAL search itself (see
|
|
25
|
+
* `NspiGetMatchesHandler.ts`) uses `RepoUtils`'s `regex(...)` operator, which compiles identically on both
|
|
26
|
+
* backends - no per-backend hook needed here.
|
|
27
|
+
*
|
|
28
|
+
* @author Jean-Philippe Steinmetz
|
|
29
|
+
*/
|
|
30
|
+
export declare abstract class BaseMapiNspiRoute<M extends Mailbox> {
|
|
31
|
+
protected abstract mailboxClass: any;
|
|
32
|
+
protected abstract contactClass: any;
|
|
33
|
+
private _objectFactory?;
|
|
34
|
+
private mailboxRepo?;
|
|
35
|
+
private contactRepo?;
|
|
36
|
+
init(): Promise<void>;
|
|
37
|
+
dispatch(req: HttpRequest, res: HttpResponse, user?: JWTUser): Promise<void>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { SimpleEntity } from "@rapidrest/service-core";
|
|
2
|
+
/** Tags what a ROP-assigned integer handle (the `ServerObjectHandleTable` index space) refers to.
|
|
3
|
+
* `entityUid` for a `"folder"` handle is one of `session.folderIds`' own value strings (`"virtual:<name>"` or
|
|
4
|
+
* `"folder:<uid>"`), not a bare UID - the same format throughout avoids a second parallel encoding. A
|
|
5
|
+
* `"table"` handle's `rows`/`columns`/`cursor` hold `RopGetHierarchyTable`/`RopSetColumns`/`RopQueryRows`
|
|
6
|
+
* state for that specific table instance: `rows` is the resolved, order-fixed list of entity targets (same
|
|
7
|
+
* `"virtual:<name>"`/`"folder:<uid>"` format) this table enumerates, `columns` the `RopSetColumns`-configured
|
|
8
|
+
* property list, `cursor` how many rows `RopQueryRows` has already returned. A `"stream"` handle's `entityUid`
|
|
9
|
+
* is the `"message:<uid>"` target its content was opened from, `propertyId`/`propertyType` the `PropertyTag`
|
|
10
|
+
* `RopOpenStream` opened (this pragmatic subset only ever supports `PidTagBody`/`PtypString`, see
|
|
11
|
+
* `MessageBodyStream.ts`), and `streamPosition` how many bytes `RopReadStream` has already returned (a
|
|
12
|
+
* read-mode stream) or `writeTargetHandleIndex`/`writeBufferBase64` the accumulated write state (a write-mode
|
|
13
|
+
* stream opened `ReadWrite`/`Create` against a `RopCreateMessage` draft's `PidTagBody` - see
|
|
14
|
+
* `RopOpenStreamHandler`'s own doc comment).
|
|
15
|
+
*
|
|
16
|
+
* A `"message"` handle from `RopCreateMessage` (a draft not yet `RopSaveChangesMessage`d) has `entityUid: ""`
|
|
17
|
+
* and instead carries `draftFolderUid` (the folder it will belong to) and `draftProperties` (the small,
|
|
18
|
+
* well-known set of properties this pragmatic subset's `RopSetProperties` tracks - Subject/DisplayTo/
|
|
19
|
+
* DisplayCc/DisplayBcc/an inline `PidTagBody`, each coerced to a plain string, keyed by decimal `PropertyId` -
|
|
20
|
+
* a string key because a JSON-object key is always a string regardless of how it's written). `writeBufferBase64`
|
|
21
|
+
* stores accumulated `RopWriteStream` bytes as base64 rather than a raw `Buffer` for the same reason `Date`
|
|
22
|
+
* fields elsewhere in this class are stored as ISO strings: `RedisCache`'s Redis-backed path round-trips
|
|
23
|
+
* everything through `JSON.stringify`/`JSON.parse`, which cannot represent a `Buffer` (or a `bigint`, which is
|
|
24
|
+
* why no property value is ever stored in its native decoded MAPI type here) losslessly.
|
|
25
|
+
*
|
|
26
|
+
* A `"fastTransfer"` handle (`RopFastTransferSourceCopyTo`/`CopyProperties`'s output handle) carries the
|
|
27
|
+
* entire pre-built FastTransfer stream eagerly (`transferBufferBase64`, same base64-for-`RedisCache` reasoning
|
|
28
|
+
* as `writeBufferBase64`) - this pragmatic subset builds the whole stream up front rather than truly
|
|
29
|
+
* incrementally, since a real client only ever pages it out via `RopFastTransferSourceGetBuffer`'s own
|
|
30
|
+
* `transferPosition` cursor into that already-complete buffer, see `FastTransferStream.ts`'s own doc comment. */
|
|
31
|
+
export interface MapiObjectHandle {
|
|
32
|
+
type: "logon" | "folder" | "message" | "table" | "stream" | "fastTransfer";
|
|
33
|
+
entityUid: string;
|
|
34
|
+
rows?: string[];
|
|
35
|
+
columns?: {
|
|
36
|
+
propertyId: number;
|
|
37
|
+
propertyType: number;
|
|
38
|
+
}[];
|
|
39
|
+
cursor?: number;
|
|
40
|
+
propertyId?: number;
|
|
41
|
+
propertyType?: number;
|
|
42
|
+
streamPosition?: number;
|
|
43
|
+
draftFolderUid?: string;
|
|
44
|
+
draftProperties?: Record<string, string>;
|
|
45
|
+
writeTargetHandleIndex?: number;
|
|
46
|
+
writeBufferBase64?: string;
|
|
47
|
+
transferBufferBase64?: string;
|
|
48
|
+
transferPosition?: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The MAPI/HTTP `Session Context` (`[MS-OXCMAPIHTTP]` §3.1.1.1): everything a `Connect`-established session
|
|
52
|
+
* needs across subsequent `Execute` requests. Never persisted to a real database - purely an ephemeral,
|
|
53
|
+
* TTL-bound cache entry (see `MapiSessionManager` below), so this deliberately extends `SimpleEntity` (just a
|
|
54
|
+
* `uid`) rather than this library's own `BaseEntity`, which would add ACL/soft-delete/optimistic-locking
|
|
55
|
+
* machinery this object has no use for.
|
|
56
|
+
*
|
|
57
|
+
* `createdAt` is a plain ISO-8601 string, not a `Date` - `RedisCache`'s Redis-backed path round-trips values
|
|
58
|
+
* through `JSON.stringify`/`JSON.parse`, which silently turns a `Date` into a string on the way out without
|
|
59
|
+
* reviving it back to a `Date` on the way in; storing it as a string from the start keeps the shape identical
|
|
60
|
+
* whether a session happens to be served from the in-memory or the Redis-backed path.
|
|
61
|
+
*/
|
|
62
|
+
export declare class MapiSessionContext extends SimpleEntity {
|
|
63
|
+
mailboxUid: string;
|
|
64
|
+
userUid: string;
|
|
65
|
+
handles: Record<number, MapiObjectHandle>;
|
|
66
|
+
nextHandleIndex: number;
|
|
67
|
+
createdAt: string;
|
|
68
|
+
/** This session's FID assignments for the 13 `RopLogon` special folders, keyed by FID (decimal string),
|
|
69
|
+
* valued `"virtual:<name>"` or `"folder:<uid>"` - see `RopLogonHandler`'s own doc comment. Populated by
|
|
70
|
+
* `RopLogon`, read back by a later `RopOpenFolder`. */
|
|
71
|
+
folderIds: Record<string, string>;
|
|
72
|
+
/** The reverse of `folderIds` (target -> FID) plus a monotonic counter, maintained alongside it by
|
|
73
|
+
* `FolderTarget.assignOrGetFid`/`RopLogonHandler` so "does this target already have a FID" is an O(1)
|
|
74
|
+
* lookup instead of a linear scan of `folderIds` repeated once per table row - see `assignOrGetFid`'s own
|
|
75
|
+
* doc comment for why the scan this replaces was a genuine (not just theoretical) quadratic cost across a
|
|
76
|
+
* session's lifetime. Small and purely additive to session state (one more string-keyed map of numbers,
|
|
77
|
+
* the same order of magnitude as `folderIds` itself), unlike caching whole resolved rows would be. */
|
|
78
|
+
folderTargetIds: Record<string, number>;
|
|
79
|
+
nextFolderId: number;
|
|
80
|
+
/** This session's MID assignments, keyed by MID (decimal string), valued `"message:<uid>"` - the message
|
|
81
|
+
* analog of `folderIds` above. Unlike `folderIds` (pre-populated by `RopLogon`), a MID only ever comes into
|
|
82
|
+
* existence lazily, the first time a `RopQueryRows` row exposes a message's `PidTagMid` column (see
|
|
83
|
+
* `MessageTarget.assignOrGetMid`), read back by a later `RopOpenMessage`. */
|
|
84
|
+
messageIds: Record<string, string>;
|
|
85
|
+
/** The reverse of `messageIds` (target -> MID) plus a monotonic counter - the message analog of
|
|
86
|
+
* `folderTargetIds`/`nextFolderId` above, for the identical O(1)-lookup reason. */
|
|
87
|
+
messageTargetIds: Record<string, number>;
|
|
88
|
+
nextMessageId: number;
|
|
89
|
+
/** This session's `RopGetPropertyIdsFromNames` mapping table (`[MS-OXCPRPT]` §2.2.12), keyed by a JSON
|
|
90
|
+
* string encoding of the `{guid, kind, lid|name}` `PropertyName` the numeric ID was assigned to - see
|
|
91
|
+
* `NamedPropertyRegistry.ts`'s own doc comment for why a JSON string (rather than a delimiter-joined one)
|
|
92
|
+
* is the safest key shape here. A real client resolves every named property (almost every Calendar-specific
|
|
93
|
+
* one - `PidLidAppointmentStartWhole`, `PidLidBusyStatus`, `PidLidAppointmentRecur`, ...) through this table
|
|
94
|
+
* once per session before ever setting/reading it via `RopSetProperties`/`RopGetPropertiesSpecific`. */
|
|
95
|
+
namedProperties: Record<string, number>;
|
|
96
|
+
/** The reverse of `namedProperties` (numeric ID -> `PropertyName` JSON key) plus a monotonic counter -
|
|
97
|
+
* `NamedPropertyRegistry.ts`'s own doc comment covers why both the O(n) forward scan this replaces and the
|
|
98
|
+
* `Math.max(...spread)` id-allocation it used are real problems, not just theoretical ones, at real
|
|
99
|
+
* mailbox/session scale. */
|
|
100
|
+
namedPropertyIds: Record<number, string>;
|
|
101
|
+
nextNamedPropertyId: number;
|
|
102
|
+
/** `mailboxUid`/`userUid` are always known at construction time (the only call site is
|
|
103
|
+
* `MapiSessionManager.create()`, which resolves both up front) - required here rather than optional with
|
|
104
|
+
* a same-value fallback, which would just be dead code no real caller ever takes the other branch of. */
|
|
105
|
+
constructor(other: Partial<SimpleEntity> & {
|
|
106
|
+
mailboxUid: string;
|
|
107
|
+
userUid: string;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Stores/loads `MapiSessionContext`s, keyed by the opaque session id that also becomes the `MapiContext`
|
|
112
|
+
* cookie value. Reuses `@rapidrest/service-core`'s existing generic `RedisCache<T>` (Redis-backed when a
|
|
113
|
+
* `cache` datastore is configured, transparent in-memory fallback otherwise) rather than either a bespoke
|
|
114
|
+
* Redis client (unlike `PingCommand`'s own throwaway pub/sub client - fine for ephemeral fan-out, not a good
|
|
115
|
+
* fit for stateful session data) or the framework's generic cookie/session middleware (built for a whole-app
|
|
116
|
+
* `req.session` object, not this protocol's specific handle-table shape). `RedisCache` needs no real
|
|
117
|
+
* persisted entity - only a class with a `.name` for its Redis key prefix - so a purely ephemeral,
|
|
118
|
+
* never-persisted class like `MapiSessionContext` is a perfectly valid `type` for it.
|
|
119
|
+
*
|
|
120
|
+
* @author Jean-Philippe Steinmetz
|
|
121
|
+
*/
|
|
122
|
+
export declare class MapiSessionManager {
|
|
123
|
+
private _objectFactory?;
|
|
124
|
+
private cache?;
|
|
125
|
+
init(): Promise<void>;
|
|
126
|
+
create(mailboxUid: string, userUid: string): Promise<MapiSessionContext>;
|
|
127
|
+
load(sessionId: string): Promise<MapiSessionContext | undefined>;
|
|
128
|
+
save(context: MapiSessionContext): Promise<void>;
|
|
129
|
+
destroy(sessionId: string): Promise<void>;
|
|
130
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RopContext, RopHandler } from "./rop/RopHandler.js";
|
|
2
|
+
/**
|
|
3
|
+
* Walks a decoded `RopBuffer`'s `ropsList` blob, dispatching each contained ROP - identified by its own
|
|
4
|
+
* leading `RopId` byte - to the matching registered handler in turn, collecting every response into one
|
|
5
|
+
* output buffer. Deliberately ROP-agnostic: it knows nothing about any specific ROP's own fields, only how to
|
|
6
|
+
* find the *next* one, via the handler's own advanced `reader` position once `handle()` returns - see
|
|
7
|
+
* `RopBuffer.ts`'s doc comment for why a fully generic split isn't possible for this wire format.
|
|
8
|
+
*
|
|
9
|
+
* An unrecognized `RopId` stops processing immediately: the bytes belonging to that ROP (and everything after
|
|
10
|
+
* it) can't be skipped without knowing that ROP's own layout, so there is no safe way to resync and continue.
|
|
11
|
+
* This is an inherent constraint of the wire format, not a design choice this library is choosing not to
|
|
12
|
+
* handle - a real client only ever sends ROPs the server has already told it (via its supported-command
|
|
13
|
+
* surface) that it understands.
|
|
14
|
+
*
|
|
15
|
+
* @author Jean-Philippe Steinmetz
|
|
16
|
+
*/
|
|
17
|
+
export declare function dispatchRops(ropsList: Buffer, handlers: Map<number, RopHandler>, context: RopContext): 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,78 @@
|
|
|
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
|
+
export declare class BufferReader {
|
|
19
|
+
private readonly buffer;
|
|
20
|
+
private offset;
|
|
21
|
+
constructor(buffer: Buffer, offset?: number);
|
|
22
|
+
get position(): number;
|
|
23
|
+
get remaining(): number;
|
|
24
|
+
hasMore(): boolean;
|
|
25
|
+
readUInt8(): number;
|
|
26
|
+
readInt16LE(): number;
|
|
27
|
+
readUInt16LE(): number;
|
|
28
|
+
readInt32LE(): number;
|
|
29
|
+
readUInt32LE(): number;
|
|
30
|
+
readFloatLE(): number;
|
|
31
|
+
readDoubleLE(): number;
|
|
32
|
+
readBigInt64LE(): bigint;
|
|
33
|
+
readBigUInt64LE(): bigint;
|
|
34
|
+
/** Unlike every fixed-width `readXxxLE` above, `Buffer.prototype.subarray` never throws - an out-of-range
|
|
35
|
+
* `length` (negative, or reaching past the buffer's end) silently clamps instead, which would otherwise let
|
|
36
|
+
* a malformed/truncated ROP buffer produce garbage-but-not-erroring reads (a negative `length` even rewinds
|
|
37
|
+
* `offset` backwards) instead of failing loudly the way this class's own doc comment promises. Explicitly
|
|
38
|
+
* bounds-checked here for that reason. */
|
|
39
|
+
readBytes(length: number): Buffer;
|
|
40
|
+
/** Reads a UTF-16LE string up to (and consuming) its terminating `0x0000` code unit - `PtypString`'s wire
|
|
41
|
+
* encoding. Scans on 2-byte boundaries, since a null code unit's low/high byte pair can't be mistaken for
|
|
42
|
+
* one half of a non-null UTF-16 code unit at an odd offset the way a naive single-byte scan could.
|
|
43
|
+
*
|
|
44
|
+
* Throws if no terminator is found before running out of buffer, rather than silently returning whatever
|
|
45
|
+
* content preceded the truncation and leaving `offset` past the buffer's actual end - a truncated/malformed
|
|
46
|
+
* string is exactly the kind of input this class's own doc comment says should fail loudly. This also
|
|
47
|
+
* closes a real DoS: `PropertyValue.ts`'s `readCountedArray` loops a client-controlled element count with
|
|
48
|
+
* no independent cap, trusting each element's own reader to terminate the loop early against a too-small
|
|
49
|
+
* buffer the way every fixed-width reader already does - without this, `PtypMultipleString`'s
|
|
50
|
+
* `readNullTerminatedUtf16LE` calls kept silently returning `""` past the buffer's end forever instead. */
|
|
51
|
+
readNullTerminatedUtf16LE(): string;
|
|
52
|
+
/** Reads a single-byte-terminated 8-bit string (`PtypString8`'s wire encoding). Multibyte string content
|
|
53
|
+
* itself is treated as UTF-8, a pragmatic choice documented in `PropertyValue.ts` rather than the
|
|
54
|
+
* "externally specified encoding" the spec leaves open-ended.
|
|
55
|
+
*
|
|
56
|
+
* Throws if no terminator is found before running out of buffer - see `readNullTerminatedUtf16LE`'s own
|
|
57
|
+
* doc comment for why (identical reasoning, including the `readCountedArray` DoS this closes). */
|
|
58
|
+
readNullTerminatedString8(): string;
|
|
59
|
+
}
|
|
60
|
+
/** The write-side counterpart to `BufferReader`, accumulating chunks and concatenating once via `toBuffer()` -
|
|
61
|
+
* the same "array of pieces, `Buffer.concat` once at the end" idiom `WbxmlEncoder` already uses for its own
|
|
62
|
+
* byte-array accumulator, adapted for multi-byte little-endian fields instead of single bytes. */
|
|
63
|
+
export declare class BufferWriter {
|
|
64
|
+
private readonly chunks;
|
|
65
|
+
writeUInt8(value: number): this;
|
|
66
|
+
writeInt16LE(value: number): this;
|
|
67
|
+
writeUInt16LE(value: number): this;
|
|
68
|
+
writeInt32LE(value: number): this;
|
|
69
|
+
writeUInt32LE(value: number): this;
|
|
70
|
+
writeFloatLE(value: number): this;
|
|
71
|
+
writeDoubleLE(value: number): this;
|
|
72
|
+
writeBigInt64LE(value: bigint): this;
|
|
73
|
+
writeBigUInt64LE(value: bigint): this;
|
|
74
|
+
writeBytes(value: Buffer): this;
|
|
75
|
+
writeNullTerminatedUtf16LE(value: string): this;
|
|
76
|
+
writeNullTerminatedString8(value: string): this;
|
|
77
|
+
toBuffer(): Buffer;
|
|
78
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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 was
|
|
7
|
+
* built from - unwrapping the `VCALID` form (see this file's own doc comment) when `Data` carries the
|
|
8
|
+
* `"vCal-Uid"` marker, falling back to treating `Data` as bare `OutlookID` bytes otherwise. Throws if
|
|
9
|
+
* `ByteArrayID` doesn't match the spec's own fixed constant - a real, spec-mandated identity check, not an
|
|
10
|
+
* invented restriction. */
|
|
11
|
+
export declare function decodeGlobalObjectId(reader: BufferReader): string;
|
|
@@ -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,77 @@
|
|
|
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
|
+
export declare function writePropertyTag(writer: BufferWriter, tag: PropertyTag): void;
|
|
54
|
+
/** `TypedPropertyValue` structure (`[MS-OXCDATA]` §2.11.4): `PropertyType` (2 bytes) followed by the value
|
|
55
|
+
* itself, encoded per `readPropertyValue`/`writePropertyValue` below. */
|
|
56
|
+
export declare function readTypedPropertyValue(reader: BufferReader): TypedPropertyValue;
|
|
57
|
+
export declare function writeTypedPropertyValue(writer: BufferWriter, typed: TypedPropertyValue): void;
|
|
58
|
+
/** `TaggedPropertyValue` structure (`[MS-OXCDATA]` §2.11.4, confirmed via `RopSetProperties`'s own request-buffer
|
|
59
|
+
* page): a full `PropertyTag` (4 bytes - `PropertyId` **and** `PropertyType`, unlike `TypedPropertyValue`'s bare
|
|
60
|
+
* `PropertyType`) followed by the value itself. Used where a property's identity can't be inferred from
|
|
61
|
+
* context (e.g. `RopSetProperties`, which sets an arbitrary, client-chosen set of properties in one call). */
|
|
62
|
+
export declare function readTaggedPropertyValue(reader: BufferReader): TaggedPropertyValue;
|
|
63
|
+
export declare function writeTaggedPropertyValue(writer: BufferWriter, tagged: TaggedPropertyValue): void;
|
|
64
|
+
/**
|
|
65
|
+
* Reads a bare `PropertyValue` (`[MS-OXCDATA]` §2.11.2) of the given, already-known `type`. `PtypBinary`'s
|
|
66
|
+
* length prefix is 16 bits and every `PtypMultiple*`'s value count is 32 bits wide, per the spec's explicit
|
|
67
|
+
* "in the context of ROP buffers" sizing rule - the only context every planned caller of this function
|
|
68
|
+
* operates in (the wider 32-bit `PtypBinary` count only applies to extended rules / a specific MAPI/HTTP
|
|
69
|
+
* structure this library doesn't implement).
|
|
70
|
+
*/
|
|
71
|
+
export declare function readPropertyValue(reader: BufferReader, type: PropertyType): PropertyValueData;
|
|
72
|
+
export declare function writePropertyValue(writer: BufferWriter, type: PropertyType, value: PropertyValueData): void;
|
|
73
|
+
/** Converts a `PtypTime` value (100-ns intervals since 1601-01-01) into a JS `Date`. Sub-millisecond
|
|
74
|
+
* precision is truncated - `Date` has no finer resolution, a real, bounded, documented gap. */
|
|
75
|
+
export declare function filetimeToDate(filetime: bigint): Date;
|
|
76
|
+
/** Converts a JS `Date` into a `PtypTime` value (100-ns intervals since 1601-01-01). */
|
|
77
|
+
export declare function dateToFiletime(date: Date): bigint;
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
export declare function encodeRopBuffer(buf: RopBuffer): Buffer;
|
|
25
|
+
export declare function decodeRopBuffer(buffer: Buffer): RopBuffer;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BufferReader, BufferWriter } from "./BufferCursor.js";
|
|
2
|
+
/**
|
|
3
|
+
* `TypedString` (`[MS-OXCDATA]` §2.11.7): a leading `StringType` byte followed by an optional string in the
|
|
4
|
+
* format that byte names - used by `RopOpenMessage`'s success response (`SubjectPrefix`/`NormalizedSubject`).
|
|
5
|
+
* `writeTypedString` never produces the "reduced Unicode" (`0x03`) form - that's purely a wire-size
|
|
6
|
+
* optimization real Exchange applies when every character happens to fit in one byte, not something a peer is
|
|
7
|
+
* ever required to send, so this codec always writes plain UTF-16LE (`0x04`) for non-empty strings. `undefined`
|
|
8
|
+
* writes as "no string" (`0x00`), matching a field the spec allows to be entirely absent (e.g. no subject
|
|
9
|
+
* prefix at all, as opposed to an empty one).
|
|
10
|
+
*/
|
|
11
|
+
export declare function writeTypedString(writer: BufferWriter, value: string | undefined): void;
|
|
12
|
+
/** Reads a `TypedString`, supporting every `StringType` value a peer could legally send (`0x00`-`0x04`) even
|
|
13
|
+
* though `writeTypedString` only ever produces `0x00`/`0x01`/`0x04` itself - this side of the codec is only
|
|
14
|
+
* ever exercised in this library's own round-trip tests, since a real MAPI/HTTP client never sends a
|
|
15
|
+
* `TypedString` value to the server. Returns `undefined` for `StringType = 0x00` ("no string"), matching
|
|
16
|
+
* `writeTypedString`'s own `undefined` input. */
|
|
17
|
+
export declare function readTypedString(reader: BufferReader): string | undefined;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAPI-over-HTTP protocol compatibility (Phase 3 of this library's roadmap): a hand-built ROP (Remote
|
|
3
|
+
* Operation) codec and buffer-framing layer (`[MS-OXCROPS]`), JWT-secured EMSMDB transport (`BaseMapiEmsmdbRoute`,
|
|
4
|
+
* reusing the framework's existing `@Auth(["jwt"])` strategy - no MAPI-specific auth code of its own), and a
|
|
5
|
+
* minimal NSPI address-book endpoint (`BaseMapiNspiRoute`) for GAL "search as you type" lookups.
|
|
6
|
+
*
|
|
7
|
+
* This module exports only the backend-agnostic surface: the wire-format codecs, the abstract route base
|
|
8
|
+
* classes, `MapiSessionManager`, the ROP dispatcher, and every ROP/NSPI handler class (each takes a generic
|
|
9
|
+
* `RopContext`/repo argument, not a concrete entity class, so none of them are backend-specific). The concrete
|
|
10
|
+
* Mongo/SQL classes a deployment actually instantiates (`MapiEmsmdbRouteMongo`/`SQL`,
|
|
11
|
+
* `MapiNspiRouteMongo`/`SQL`) are exported from this package's `./mongo`/`./sql` subpaths instead, alongside
|
|
12
|
+
* every other entity/route/job this library defines - see `src/eas/index.ts`'s identical doc comment for the
|
|
13
|
+
* same convention.
|
|
14
|
+
*
|
|
15
|
+
* A deployment mounts both MAPI endpoints with a trivial one-line subclass each, the same pattern
|
|
16
|
+
* `src/eas/index.ts`'s doc comment describes for `BaseEasRoute`:
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { MapiEmsmdbRouteMongo, MapiNspiRouteMongo } from "@rapidrest/mail/mongo";
|
|
19
|
+
* import { RouteDecorators } from "@rapidrest/service-core";
|
|
20
|
+
* const { Route } = RouteDecorators;
|
|
21
|
+
*
|
|
22
|
+
* @Route("/mapi/emsmdb")
|
|
23
|
+
* export class MyMapiEmsmdbRoute extends MapiEmsmdbRouteMongo {}
|
|
24
|
+
*
|
|
25
|
+
* @Route("/mapi/nspi")
|
|
26
|
+
* export class MyMapiNspiRoute extends MapiNspiRouteMongo {}
|
|
27
|
+
* ```
|
|
28
|
+
* Real Outlook desktop locates both URLs via `@rapidrest/mail/autodiscover`'s Outlook/EXCH response
|
|
29
|
+
* (`BaseAutodiscoverRoute`'s `mapiUrl` property) - see that module's own doc comment.
|
|
30
|
+
*
|
|
31
|
+
* **Documented gaps in this pragmatic subset** (see the architecture plan's "MAPI over HTTP (Phase 3)" section
|
|
32
|
+
* for the full reasoning behind each):
|
|
33
|
+
* - Incremental sync (`RopFastTransferSourceCopyTo`/`CopyProperties`/`GetBuffer`) always builds a full,
|
|
34
|
+
* non-differential dump, not real IDSET-based ICS (`[MS-OXCFXICS]`) - a real client still works correctly
|
|
35
|
+
* against this, just less efficiently than byte-perfect ICS.
|
|
36
|
+
* - Calendar/meetings: no counter-proposals, meeting-forwarding, delegate scheduling, or resource-booking
|
|
37
|
+
* auto-accept; no DST-aware timezones (fixed-offset approximation only); no recurrence exceptions/modified
|
|
38
|
+
* instances; no Hijri/`MonthEnd` recurrence patterns.
|
|
39
|
+
* - No `RopModifyRecipients` - attendees/recipients are always added via the same `PidTagDisplayTo`/`Cc`
|
|
40
|
+
* pragmatic path used for ordinary mail.
|
|
41
|
+
* - No public-folder support, no delegate/shared-mailbox access, no rules/permissions/search-folder ROPs, no
|
|
42
|
+
* client-certificate enrollment.
|
|
43
|
+
* - Calendar named properties (location/recurrence/timezone/etc.) are excluded from FastTransfer streams -
|
|
44
|
+
* only the columns in `FastTransferStream.ts`'s `DEFAULT_*_COLUMNS` tables are included.
|
|
45
|
+
* - NSPI is limited to `Bind`/`Unbind`/`GetMatches` only - no row/name lookups beyond a single content-
|
|
46
|
+
* restriction search, no directory replication.
|
|
47
|
+
*/
|
|
48
|
+
export * from "./codec/BufferCursor.js";
|
|
49
|
+
export * from "./codec/MapiGuid.js";
|
|
50
|
+
export * from "./codec/TypedString.js";
|
|
51
|
+
export * from "./codec/PropertyValue.js";
|
|
52
|
+
export * from "./codec/RopBuffer.js";
|
|
53
|
+
export * from "./codec/AppointmentRecurrence.js";
|
|
54
|
+
export * from "./codec/MapiTimeZone.js";
|
|
55
|
+
export * from "./codec/GlobalObjectId.js";
|
|
56
|
+
export * from "./RopDispatcher.js";
|
|
57
|
+
export * from "./MapiSessionManager.js";
|
|
58
|
+
export * from "./BaseMapiEmsmdbRoute.js";
|
|
59
|
+
export * from "./BaseMapiNspiRoute.js";
|
|
60
|
+
export * from "./rop/RopHandler.js";
|
|
61
|
+
export * from "./rop/FolderTarget.js";
|
|
62
|
+
export * from "./rop/MessageTarget.js";
|
|
63
|
+
export * from "./rop/CalendarEventTarget.js";
|
|
64
|
+
export * from "./rop/MessageBodyStream.js";
|
|
65
|
+
export * from "./rop/PropertyResolvers.js";
|
|
66
|
+
export * from "./rop/NamedPropertyRegistry.js";
|
|
67
|
+
export * from "./rop/CalendarNamedProperties.js";
|
|
68
|
+
export * from "./rop/FastTransferStream.js";
|
|
69
|
+
export * from "./rop/MeetingMessageClassHandler.js";
|
|
70
|
+
export * from "./rop/RopLogonHandler.js";
|
|
71
|
+
export * from "./rop/RopReleaseHandler.js";
|
|
72
|
+
export * from "./rop/RopOpenFolderHandler.js";
|
|
73
|
+
export * from "./rop/RopGetHierarchyTableHandler.js";
|
|
74
|
+
export * from "./rop/RopSetColumnsHandler.js";
|
|
75
|
+
export * from "./rop/RopQueryRowsHandler.js";
|
|
76
|
+
export * from "./rop/RopGetContentsTableHandler.js";
|
|
77
|
+
export * from "./rop/RopOpenMessageHandler.js";
|
|
78
|
+
export * from "./rop/RopGetPropertiesSpecificHandler.js";
|
|
79
|
+
export * from "./rop/RopOpenStreamHandler.js";
|
|
80
|
+
export * from "./rop/RopReadStreamHandler.js";
|
|
81
|
+
export * from "./rop/RopCreateMessageHandler.js";
|
|
82
|
+
export * from "./rop/RopSetPropertiesHandler.js";
|
|
83
|
+
export * from "./rop/RopWriteStreamHandler.js";
|
|
84
|
+
export * from "./rop/RopSaveChangesMessageHandler.js";
|
|
85
|
+
export * from "./rop/RopSubmitMessageHandler.js";
|
|
86
|
+
export * from "./rop/RopGetPropertyIdsFromNamesHandler.js";
|
|
87
|
+
export * from "./rop/RopDeleteMessagesHandler.js";
|
|
88
|
+
export * from "./rop/RopDeleteFolderHandler.js";
|
|
89
|
+
export * from "./rop/RopFastTransferSourceCopyToHandler.js";
|
|
90
|
+
export * from "./rop/RopFastTransferSourceCopyPropertiesHandler.js";
|
|
91
|
+
export * from "./rop/RopFastTransferSourceGetBufferHandler.js";
|
|
92
|
+
export * from "./nspi/NspiCodec.js";
|
|
93
|
+
export * from "./nspi/NspiBindHandler.js";
|
|
94
|
+
export * from "./nspi/NspiGetMatchesHandler.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MailboxMongo } from "@rapidmx/restapi/mongo";
|
|
2
|
+
import { BaseMapiEmsmdbRoute } from "../BaseMapiEmsmdbRoute.js";
|
|
3
|
+
/**
|
|
4
|
+
* Mongo-backed concrete `BaseMapiEmsmdbRoute`, mounted at `/mapi/emsmdb`. Exported from this plugin's
|
|
5
|
+
* `./mongo` entry point, so the server host mounts it without a wrapper class of its
|
|
6
|
+
* own.
|
|
7
|
+
*
|
|
8
|
+
* @author Jean-Philippe Steinmetz
|
|
9
|
+
*/
|
|
10
|
+
export declare class MapiEmsmdbRouteMongo extends BaseMapiEmsmdbRoute<MailboxMongo> {
|
|
11
|
+
protected mailboxClass: any;
|
|
12
|
+
protected folderClass: any;
|
|
13
|
+
protected messageClass: any;
|
|
14
|
+
protected calendarEventClass: any;
|
|
15
|
+
protected contactClass: any;
|
|
16
|
+
protected taskClass: any;
|
|
17
|
+
protected labelClass: any;
|
|
18
|
+
protected ropHandlerClasses: any[];
|
|
19
|
+
}
|