@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,13 @@
|
|
|
1
|
+
import { MailboxMongo } from "@rapidmx/restapi/mongo";
|
|
2
|
+
import { BaseMapiNspiRoute } from "../BaseMapiNspiRoute.js";
|
|
3
|
+
/**
|
|
4
|
+
* Mongo-backed concrete `BaseMapiNspiRoute`, mounted at `/mapi/nspi`. 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 MapiNspiRouteMongo extends BaseMapiNspiRoute<MailboxMongo> {
|
|
11
|
+
protected mailboxClass: any;
|
|
12
|
+
protected contactClass: any;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./mongo/index.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { JWTUser } from "@rapidrest/core";
|
|
2
|
+
import type { HttpResponse, RepoUtils } from "@rapidrest/service-core";
|
|
3
|
+
/**
|
|
4
|
+
* `Bind`/`Unbind` request types (`[MS-OXCMAPIHTTP]` §2.2.5.1/§2.2.5.2, backed by `[MS-OXNSPI]`'s own `NspiBind`/
|
|
5
|
+
* `NspiUnbind` methods): establishes/tears down an NSPI Session Context, the address-book endpoint's own
|
|
6
|
+
* analog of EMSMDB's `Connect`/`Disconnect`.
|
|
7
|
+
*
|
|
8
|
+
* **No real session state is created or tracked** - a deliberate, documented simplification, not an oversight:
|
|
9
|
+
* every subsequent NSPI operation in this pragmatic subset (`GetMatches`) is independently `@Auth(["jwt"])`-
|
|
10
|
+
* protected and re-resolves the caller's own mailbox from that same JWT, exactly the way `RopLogon`/every ROP
|
|
11
|
+
* handler already does for EMSMDB - the real spec's `MapiContext`-equivalent session cookie exists mainly for
|
|
12
|
+
* multi-server-farm request affinity, not additional authentication, so this pragmatic subset mints an opaque,
|
|
13
|
+
* unvalidated cookie value purely for wire-format conformance and never checks it again. `HasState`/`State`
|
|
14
|
+
* (an optional `STAT` scoping the bind to one address-book container) is accepted but not decoded at all -
|
|
15
|
+
* this pragmatic subset has exactly one "container" (a mailbox's own `Contact` list) regardless.
|
|
16
|
+
*
|
|
17
|
+
* @author Jean-Philippe Steinmetz
|
|
18
|
+
*/
|
|
19
|
+
export declare function handleNspiBind(res: HttpResponse, user: JWTUser, mailboxRepo: RepoUtils<any>): Promise<void>;
|
|
20
|
+
/** `Unbind` never fails and has nothing to tear down - see `handleNspiBind`'s own doc comment for why no real
|
|
21
|
+
* session exists to release. */
|
|
22
|
+
export declare function handleNspiUnbind(res: HttpResponse): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { BufferReader, BufferWriter } from "../codec/BufferCursor.js";
|
|
2
|
+
import { type PropertyTag, type PropertyValueData } from "../codec/PropertyValue.js";
|
|
3
|
+
/**
|
|
4
|
+
* Shared wire-format helpers for the NSPI-over-HTTP address-book endpoint (`[MS-OXCMAPIHTTP]` §2.2.5,
|
|
5
|
+
* `[MS-OXNSPI]`) - a genuinely different transport shape from EMSMDB's ROP buffers: each `X-RequestType` value
|
|
6
|
+
* (`Bind`/`Unbind`/`GetMatches`/...) has its own flat request/response body, not a multiplexed ROP stream, so
|
|
7
|
+
* there is no generic framing codec to share the way `RopBuffer.ts` does for EMSMDB - just these small
|
|
8
|
+
* structures every NSPI operation body embeds.
|
|
9
|
+
*
|
|
10
|
+
* @author Jean-Philippe Steinmetz
|
|
11
|
+
*/
|
|
12
|
+
/** `STAT` (`[MS-OXNSPI]` §2.2.8, confirmed this session): 9 `DWORD`-sized fields (36 bytes total, `Delta`
|
|
13
|
+
* signed, every other field unsigned) describing an address-book container's paging/locale state. This
|
|
14
|
+
* pragmatic subset never persists a `STAT` across calls (see `NspiGetMatchesHandler.ts`'s own doc comment for
|
|
15
|
+
* why) - `readStat`/`writeStat` exist purely to keep the surrounding request/response body's own field
|
|
16
|
+
* offsets correct, not to drive any real positioning logic. */
|
|
17
|
+
export interface Stat {
|
|
18
|
+
sortType: number;
|
|
19
|
+
containerId: number;
|
|
20
|
+
currentRec: number;
|
|
21
|
+
delta: number;
|
|
22
|
+
numPos: number;
|
|
23
|
+
totalRecs: number;
|
|
24
|
+
codePage: number;
|
|
25
|
+
templateLocale: number;
|
|
26
|
+
sortLocale: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function readStat(reader: BufferReader): Stat;
|
|
29
|
+
export declare function writeStat(writer: BufferWriter, stat: Stat): void;
|
|
30
|
+
/** A blank `STAT` (`SortType`/`ContainerID`/... all zero) - what this pragmatic subset always echoes back
|
|
31
|
+
* (in `Bind`'s optional acknowledgement and `GetMatches`' response) regardless of the caller's own request
|
|
32
|
+
* `STAT`, since no real per-container/locale state is tracked. */
|
|
33
|
+
export declare const BLANK_STAT: Stat;
|
|
34
|
+
/** `LargePropertyTagArray` (`[MS-OXCMAPIHTTP]` §2.2.1.8): a 4-byte count followed by that many `PropertyTag`
|
|
35
|
+
* structures (`[MS-OXCDATA]` §2.9 - the same `PropertyType`-then-`PropertyId` encoding `PropertyValue.ts`'s own
|
|
36
|
+
* `readPropertyTag`/`writePropertyTag` already implement). */
|
|
37
|
+
export declare function readLargePropertyTagArray(reader: BufferReader): PropertyTag[];
|
|
38
|
+
export declare function writeLargePropertyTagArray(writer: BufferWriter, tags: PropertyTag[]): void;
|
|
39
|
+
/** Writes one `AddressBookPropertyRow` (`[MS-OXCMAPIHTTP]` §2.2.1.7) - always `Flags=0x00` (every column
|
|
40
|
+
* value present without error), the exact `StandardPropertyRow`-equivalent choice `RopQueryRowsHandler`/
|
|
41
|
+
* `RopGetPropertiesSpecificHandler` already make for EMSMDB's own row encoding. */
|
|
42
|
+
export declare function writeAddressBookPropertyRow(writer: BufferWriter, columns: PropertyTag[], values: PropertyValueData[]): void;
|
|
43
|
+
/** Decodes a `Restriction` (`[MS-OXCDATA]` §2.12)'s leading `RestrictType` byte and, only for a
|
|
44
|
+
* `ContentRestriction`/`RES_CONTENT` (`0x03`) - the shape a real client's own GAL "search as you type" sends -
|
|
45
|
+
* extracts the plain string search term from its `TaggedValue` field.
|
|
46
|
+
*
|
|
47
|
+
* **Throws for any other restriction type** (`AND`/`OR`/`PropertyRestriction`/...) rather than degrading
|
|
48
|
+
* gracefully - unlike this pragmatic subset's usual "return a default instead of failing" stance elsewhere,
|
|
49
|
+
* a restriction's own byte length is type-dependent (12 different packet formats, several recursively nested),
|
|
50
|
+
* so once an unrecognized `RestrictType` is seen there is no safe way to know how many bytes to skip to reach
|
|
51
|
+
* the request body's own subsequent fields (`RowCount`/`Columns`/...) - silently returning `undefined` here
|
|
52
|
+
* would leave the reader mid-structure and corrupt every field decoded afterward. Throwing is the honest,
|
|
53
|
+
* safe choice for an input this narrow codec was never meant to parse, not a design gap.
|
|
54
|
+
*
|
|
55
|
+
* `FuzzyLevelLow`/`FuzzyLevelHigh` (exact/substring/prefix matching, case sensitivity) and the restriction's
|
|
56
|
+
* own target `PropertyTag` are decoded to advance the reader correctly but not honored - this pragmatic
|
|
57
|
+
* subset always does a case-insensitive substring match against its own fixed Contact field set regardless of
|
|
58
|
+
* which property or fuzzy level the client's restriction actually named. */
|
|
59
|
+
export declare function extractContentRestrictionSearchTerm(reader: BufferReader): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { HttpRequest, HttpResponse, RepoUtils } from "@rapidrest/service-core";
|
|
2
|
+
/**
|
|
3
|
+
* `GetMatches` request type (`[MS-OXCMAPIHTTP]` §2.2.5.5, backed by `[MS-OXNSPI]`'s own `NspiGetMatches`
|
|
4
|
+
* method): searches the GAL (this library's `Contact` records, the same source of truth EAS's own `Search`
|
|
5
|
+
* command already uses) for entries matching the client's restriction, returning up to `RowCount` matches as
|
|
6
|
+
* `AddressBookPropertyRow`s. This is the real operation behind a client's own address-book "search as you
|
|
7
|
+
* type" UI - the closest real NSPI equivalent to this plan's own "minimal prefix/substring lookups" scope.
|
|
8
|
+
*
|
|
9
|
+
* `HasState`/`State` (an input `STAT` scoping the search and reporting position) and `HasMinimalIds`/
|
|
10
|
+
* `MinimalIds` (an "Explicit Table" restricting the search to a caller-supplied candidate set) are decoded to
|
|
11
|
+
* advance the reader correctly but not honored - this pragmatic subset always searches the caller's entire
|
|
12
|
+
* Contact list fresh on every call (no real per-container/table state, see `NspiBindHandler.ts`'s own doc
|
|
13
|
+
* comment for the same reasoning applied to `Bind`). `HasFilter`'s `Filter` is decoded via
|
|
14
|
+
* `NspiCodec.extractContentRestrictionSearchTerm` - see that function's own doc comment for why an
|
|
15
|
+
* unsupported restriction type throws rather than degrading. `HasPropertyName` (an alternate open-by-named-
|
|
16
|
+
* property addressing mode) is decoded to advance the reader correctly but never itself used to select a
|
|
17
|
+
* search field.
|
|
18
|
+
*
|
|
19
|
+
* The response's own `State` is always `BLANK_STAT` (with `TotalRecs` set to the real match count) - no real
|
|
20
|
+
* per-container `STAT` is tracked, matching this pragmatic subset's stated scope.
|
|
21
|
+
*
|
|
22
|
+
* @author Jean-Philippe Steinmetz
|
|
23
|
+
*/
|
|
24
|
+
export declare function handleNspiGetMatches(req: HttpRequest, res: HttpResponse, mailboxUid: string, contactRepo: RepoUtils<any>): Promise<void>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RepoUtils } from "@rapidrest/service-core";
|
|
2
|
+
import { Attendee, BusyStatus, RecurrenceRule } from "@rapidmx/restapi";
|
|
3
|
+
/**
|
|
4
|
+
* The `RopGetContentsTable`/`RopOpenMessage` analog of `MessageTarget.ts`, for a `Folder` of type `CALENDAR`:
|
|
5
|
+
* resolves a `"calendarEvent:<uid>"` row/handle target into the display data a calendar-item row or an opened
|
|
6
|
+
* appointment's properties need. A `CalendarEvent` is addressed by MID exactly the same way a `Message` is -
|
|
7
|
+
* `MessageTarget.assignOrGetMid`/`session.messageIds` are reused as-is (see that file's own doc comment; the
|
|
8
|
+
* mechanism is already generic over the target-string prefix, not message-specific despite its file location),
|
|
9
|
+
* so this file adds no MID-registry code of its own.
|
|
10
|
+
*/
|
|
11
|
+
export interface CalendarEventTargetInfo {
|
|
12
|
+
title: string;
|
|
13
|
+
location?: string;
|
|
14
|
+
startDate: Date;
|
|
15
|
+
endDate: Date;
|
|
16
|
+
timezone: string;
|
|
17
|
+
busyStatus: BusyStatus;
|
|
18
|
+
recurrenceRule?: RecurrenceRule;
|
|
19
|
+
reminderMinutesBeforeStart?: number;
|
|
20
|
+
organizerAddress: string;
|
|
21
|
+
attendees: Attendee[];
|
|
22
|
+
}
|
|
23
|
+
/** Degrades to empty-looking values for a `"calendarEvent:<uid>"` target whose real `CalendarEvent` has since
|
|
24
|
+
* vanished (soft-deleted or otherwise) - the same "don't fail the whole ROP over one stale row" principle
|
|
25
|
+
* `MessageTarget.resolveMessageInfo`/`FolderTarget.resolveFolderInfo` already apply. */
|
|
26
|
+
export declare function resolveCalendarEventInfo(target: string, calendarEventRepo: RepoUtils<any>): Promise<CalendarEventTargetInfo>;
|
|
27
|
+
/** Resolves the calendar events directly in `folderUid`, as `"calendarEvent:<uid>"` target strings, for
|
|
28
|
+
* `RopGetContentsTable` against a `CALENDAR`-type folder. */
|
|
29
|
+
export declare function resolveFolderCalendarEvents(folderUid: string, calendarEventRepo: RepoUtils<any>): Promise<string[]>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { AttendeeResponseStatus, BusyStatus } from "@rapidmx/restapi";
|
|
2
|
+
/**
|
|
3
|
+
* The Calendar named-property identity table (`(PropertySet GUID, LID)` pairs) this pragmatic subset supports,
|
|
4
|
+
* per `[MS-OXPROPS]` - each pinned against its own MS-OXPROPS page (not invented), shared by `PropertyResolvers.ts`
|
|
5
|
+
* (the read side: `calendarEventValueFor`) and `RopSaveChangesMessageHandler.ts` (the write side: decoding a
|
|
6
|
+
* draft's accumulated `RopSetProperties` values back into a real `CalendarEvent`) so both directions of this
|
|
7
|
+
* mapping stay in exactly one place. See the architecture plan's "Calendar support" section for the full table.
|
|
8
|
+
*/
|
|
9
|
+
export declare const PSETID_APPOINTMENT = "00062002-0000-0000-c000-000000000046";
|
|
10
|
+
export declare const PSETID_COMMON = "00062008-0000-0000-c000-000000000046";
|
|
11
|
+
export declare const PSETID_MEETING = "6ed8da90-450b-101b-98da-00aa003f1305";
|
|
12
|
+
export declare const LID_LOCATION = 33288;
|
|
13
|
+
export declare const LID_APPOINTMENT_START_WHOLE = 33293;
|
|
14
|
+
export declare const LID_APPOINTMENT_END_WHOLE = 33294;
|
|
15
|
+
export declare const LID_BUSY_STATUS = 33285;
|
|
16
|
+
export declare const LID_RECURRING = 33315;
|
|
17
|
+
export declare const LID_REMINDER_SET = 34051;
|
|
18
|
+
export declare const LID_REMINDER_DELTA = 34049;
|
|
19
|
+
export declare const LID_RESPONSE_STATUS = 33304;
|
|
20
|
+
export declare const LID_APPOINTMENT_RECUR = 33302;
|
|
21
|
+
export declare const LID_TIME_ZONE_STRUCT = 33331;
|
|
22
|
+
/** `PidLidGlobalObjectId` (`PSETID_Meeting`) - see `GlobalObjectId.ts` for the BLOB it carries and
|
|
23
|
+
* `MeetingMessageClassHandler.ts` for how a meeting response's own copy of it is correlated back to the
|
|
24
|
+
* original `CalendarEvent`. */
|
|
25
|
+
export declare const LID_GLOBAL_OBJECT_ID = 3;
|
|
26
|
+
/** `PidLidBusyStatus`'s wire values (`[MS-OXOCAL]` §2.2.1.2, confirmed this session) - a different numbering
|
|
27
|
+
* than `CalendarSyncAdapter`'s own `BUSY_STATUS_CODES` table for the unrelated MS-ASCAL `BusyStatus` field.
|
|
28
|
+
* `olWorkingElsewhere` (0x00000004) has no equivalent in this library's own `BusyStatus` enum - never produced,
|
|
29
|
+
* and decoded (via `BUSY_STATUS_FROM_CODE`) with a fallback to `BUSY`, a documented, harmless approximation. */
|
|
30
|
+
export declare const BUSY_STATUS_CODES: Record<BusyStatus, number>;
|
|
31
|
+
export declare const BUSY_STATUS_FROM_CODE: Record<number, BusyStatus>;
|
|
32
|
+
/** `PidLidResponseStatus`'s wire values (`[MS-OXOCAL]` §2.2.1.11, confirmed this session) for the caller's own
|
|
33
|
+
* attendee record. `respOrganized` (0x00000001) is returned when the caller *is* the organizer - not a value
|
|
34
|
+
* `AttendeeResponseStatus` itself has a case for, since that enum only models an attendee's own response. */
|
|
35
|
+
export declare const RESPONSE_STATUS_CODES: Record<AttendeeResponseStatus, number>;
|
|
36
|
+
export declare const RESPONSE_STATUS_ORGANIZED = 1;
|
|
37
|
+
export declare const RESPONSE_STATUS_NONE = 0;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RepoUtils } from "@rapidrest/service-core";
|
|
2
|
+
/**
|
|
3
|
+
* The `RopGetContentsTable`/`RopOpenMessage` analog of `MessageTarget.ts`, for a `Folder` of type `CONTACTS`:
|
|
4
|
+
* resolves a `"contact:<uid>"` row/handle target into the display data a contact-item row or an opened
|
|
5
|
+
* address-card's properties need. A `Contact` is addressed by MID exactly the same way a `Message`/
|
|
6
|
+
* `CalendarEvent` is - `MessageTarget.assignOrGetMid`/`session.messageIds` are reused as-is (see that file's own
|
|
7
|
+
* doc comment), so this file adds no MID-registry code of its own.
|
|
8
|
+
*
|
|
9
|
+
* Read-only in this pragmatic subset - a client editing/creating a Contact via MAPI (`RopSetProperties`/
|
|
10
|
+
* `RopSaveChangesMessage` against a `"contact:"` handle) is a documented gap, the same "browse only" limitation
|
|
11
|
+
* this codebase already accepts for Tasks (`TaskTarget.ts`); NSPI's own `Bind`/`Unbind`/`GetMatches` already
|
|
12
|
+
* covers the "search the GAL" case this data also backs.
|
|
13
|
+
*/
|
|
14
|
+
export interface ContactTargetInfo {
|
|
15
|
+
displayName: string;
|
|
16
|
+
givenName?: string;
|
|
17
|
+
surname?: string;
|
|
18
|
+
email?: string;
|
|
19
|
+
businessPhone?: string;
|
|
20
|
+
companyName?: string;
|
|
21
|
+
jobTitle?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Degrades to empty-looking values for a `"contact:<uid>"` target whose real `Contact` has since vanished
|
|
24
|
+
* (soft-deleted or otherwise) - the same "don't fail the whole ROP over one stale row" principle
|
|
25
|
+
* `MessageTarget.resolveMessageInfo`/`CalendarEventTarget.resolveCalendarEventInfo` already apply. */
|
|
26
|
+
export declare function resolveContactInfo(target: string, contactRepo: RepoUtils<any>): Promise<ContactTargetInfo>;
|
|
27
|
+
/** Resolves the contacts directly in `folderUid`, as `"contact:<uid>"` target strings, for
|
|
28
|
+
* `RopGetContentsTable` against a `CONTACTS`-type folder. */
|
|
29
|
+
export declare function resolveFolderContacts(folderUid: string, contactRepo: RepoUtils<any>): Promise<string[]>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { PropertyType } from "../codec/PropertyValue.js";
|
|
2
|
+
import type { MapiObjectHandle } from "../MapiSessionManager.js";
|
|
3
|
+
import type { RopContext } from "./RopHandler.js";
|
|
4
|
+
interface PropertyColumn {
|
|
5
|
+
propertyId: number;
|
|
6
|
+
propertyType: PropertyType;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Builds the complete FastTransfer stream for `handle` (a `"folder"` or `"message"` Server object). `columns`,
|
|
10
|
+
* when given (a `RopFastTransferSourceCopyProperties` explicit include list), replaces every default column set
|
|
11
|
+
* uniformly at both folder- and message-level; `excludePropertyIds` (a `RopFastTransferSourceCopyTo` exclude
|
|
12
|
+
* list) is only ever applied to this pragmatic subset's own default columns, per the same reasoning.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildFastTransferStream(handle: MapiObjectHandle, context: RopContext, options?: {
|
|
15
|
+
columns?: PropertyColumn[];
|
|
16
|
+
excludePropertyIds?: ReadonlySet<number>;
|
|
17
|
+
}): Promise<Buffer>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { RepoUtils } from "@rapidrest/service-core";
|
|
2
|
+
import { Folder } from "@rapidmx/restapi";
|
|
3
|
+
import type { MapiSessionContext } from "../MapiSessionManager.js";
|
|
4
|
+
export interface FolderTargetInfo {
|
|
5
|
+
displayName: string;
|
|
6
|
+
unreadCount: number;
|
|
7
|
+
totalCount: number;
|
|
8
|
+
hasChildren: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** An optional, purely request-scoped (never persisted to the session) memo for `resolveFolderChildren`'s own
|
|
11
|
+
* full-mailbox `folderRepo.find({ mailboxUid })` fetch - see that function's own doc comment for why this
|
|
12
|
+
* matters. Deliberately just a plain object a caller creates fresh per ROP-handler invocation and threads
|
|
13
|
+
* through every `resolveFolderInfo`/`resolvePropertyValues` call in that same batch, not session state - unlike
|
|
14
|
+
* the FID/MID/named-property registries, there's no cross-request reuse to gain here (a mailbox's folder list
|
|
15
|
+
* can change between requests), only cross-*row* reuse within one table page. */
|
|
16
|
+
export interface FolderResolutionCache {
|
|
17
|
+
allFolders?: Folder[];
|
|
18
|
+
}
|
|
19
|
+
/** Resolves a target string into the display data a folder-table row needs. A `"folder:<uid>"` target whose
|
|
20
|
+
* `Folder` has since been deleted (soft-deleted or otherwise vanished) degrades to empty-looking values rather
|
|
21
|
+
* than throwing - the row simply won't be interesting to a client, not a reason to fail the whole ROP. */
|
|
22
|
+
export declare function resolveFolderInfo(mailboxUid: string, target: string, folderRepo: RepoUtils<any>, cache?: FolderResolutionCache): Promise<FolderTargetInfo>;
|
|
23
|
+
/**
|
|
24
|
+
* Resolves the direct children of `target`, as an array of the same target-string format, for
|
|
25
|
+
* `RopGetHierarchyTable`. Only `"virtual:root"`/`"virtual:ipmSubtree"` (this mailbox's top-level real
|
|
26
|
+
* folders - both collapse to the same "top of the visible tree" concept in this pragmatic subset, see
|
|
27
|
+
* `RopLogonHandler`'s own doc comment) and a real folder (its own real children) have any children at all;
|
|
28
|
+
* every other virtual folder (Deferred Action, Spooler Queue, ...) is permanently empty, since this data
|
|
29
|
+
* model has no concept of nesting anything under them.
|
|
30
|
+
*
|
|
31
|
+
* Filters an already-fetched full folder list in application code rather than querying by
|
|
32
|
+
* `parentFolderUid: undefined` directly - query-DSL semantics for "field is unset" aren't reliably consistent
|
|
33
|
+
* across backends (the exact kind of gap this project's own testing philosophy has caught before, e.g. `$or`
|
|
34
|
+
* being Mongo-only), so filtering a fetched array sidesteps the question entirely rather than risking it.
|
|
35
|
+
* The comparison itself uses `== null` (matching both `null` and `undefined`), not `=== undefined` - a
|
|
36
|
+
* top-level folder's unset `parentFolderUid` round-trips as genuine `undefined` from Mongo but as `null` from
|
|
37
|
+
* a SQL `nullable` column, a real, previously-confirmed cross-backend discrepancy in this codebase (caught by
|
|
38
|
+
* this exact test against a real SQLite-backed server, not assumed).
|
|
39
|
+
*
|
|
40
|
+
* **`cache`**: without it, this fetches every folder in the mailbox on every call - fine for the single call
|
|
41
|
+
* `RopGetHierarchyTableHandler` makes to build a table, but `resolveFolderInfo` also calls this once per row
|
|
42
|
+
* just to compute `hasChildren`, and a table page can hold many rows. A caller resolving a whole page (
|
|
43
|
+
* `RopQueryRowsHandler`/`RopGetPropertiesSpecificHandler`) passes one shared cache object through every call in
|
|
44
|
+
* that batch so the full list is fetched at most once per batch instead of once per row - see
|
|
45
|
+
* `FolderResolutionCache`'s own doc comment for why this isn't session state.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveFolderChildren(mailboxUid: string, target: string, folderRepo: RepoUtils<any>, cache?: FolderResolutionCache): Promise<string[]>;
|
|
48
|
+
/**
|
|
49
|
+
* Returns `target`'s existing FID if `RopLogon` or an earlier `RopGetHierarchyTable` row already assigned
|
|
50
|
+
* one, otherwise assigns and remembers the next free small integer FID. This is what lets a client
|
|
51
|
+
* `RopOpenFolder` a child folder it only ever learned about via a `RopQueryRows` row's `PidTagFolderId`
|
|
52
|
+
* column - without this, only the 13 `RopLogon`-time special folders could ever be opened.
|
|
53
|
+
*
|
|
54
|
+
* Backed by `session.folderTargetIds` (target -> FID) and `session.nextFolderId`, an O(1) reverse index/counter
|
|
55
|
+
* pair rather than a linear scan of `session.folderIds` plus a `Math.max(...spread)` over its keys - both real
|
|
56
|
+
* costs, not just theoretical ones: `folderTargetIds` only ever grows for a session's lifetime as a client
|
|
57
|
+
* browses more of a mailbox's folder tree, so the old scan-per-lookup approach was quadratic across a session
|
|
58
|
+
* that opens many folders, and `Math.max` spreading an unbounded array as call arguments risked a stack
|
|
59
|
+
* overflow past V8's argument-count limit on a mailbox with enough folders. `RopLogonHandler` keeps
|
|
60
|
+
* `folderTargetIds`/`nextFolderId` in sync with `folderIds` for the 13 special folders it assigns directly,
|
|
61
|
+
* rather than going through this function, since it assigns all 13 as one batch up front.
|
|
62
|
+
*/
|
|
63
|
+
export declare function assignOrGetFid(session: MapiSessionContext, target: string): number;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { RopContext } from "./RopHandler.js";
|
|
2
|
+
/**
|
|
3
|
+
* Handles a submitted `"IPM.Schedule.Meeting.Resp.{Pos,Neg,Tent}"` message - an attendee's own response to a
|
|
4
|
+
* meeting invite this server previously sent (`RopSubmitMessageHandler.submitAppointment`). Delegated to from
|
|
5
|
+
* `RopSubmitMessageHandler` for that message-class prefix, instead of the ordinary mail or Appointment paths.
|
|
6
|
+
*
|
|
7
|
+
* Correlates the response back to the original `CalendarEvent` via `PidLidGlobalObjectId` (echoed by a real
|
|
8
|
+
* client from the invite it's responding to - see `GlobalObjectId.ts`'s own doc comment for why this requires
|
|
9
|
+
* no separate correlation table) and updates the caller's own `Attendee.responseStatus` in place, reusing the
|
|
10
|
+
* exact same matching-by-mailbox-address logic `MeetingResponseCommand` (EAS's own `MeetingResponse` handler)
|
|
11
|
+
* already implements - this is a from-scratch reimplementation of that small algorithm rather than a
|
|
12
|
+
* cross-import, since EAS and MAPI are independent protocol verticals in this codebase with no dependency
|
|
13
|
+
* between them.
|
|
14
|
+
*
|
|
15
|
+
* Every failure mode here (no `PidLidGlobalObjectId` set, no matching `CalendarEvent`, no resolvable mailbox,
|
|
16
|
+
* caller isn't actually an attendee, unrecognized message-class suffix) is a silent no-op rather than an error
|
|
17
|
+
* response - the same "don't fail the whole ROP over a stale/unresolvable reference" principle this pragmatic
|
|
18
|
+
* subset applies throughout (e.g. `MessageTarget.resolveMessageInfo`'s own doc comment). A response that can't
|
|
19
|
+
* be correlated or applied is simply dropped, not a reason to reject the client's `RopSubmitMessage` call.
|
|
20
|
+
*
|
|
21
|
+
* **The one query in this package that isn't `mailboxUid`-scoped, and why that's unavoidable here**: unlike
|
|
22
|
+
* every other `ignoreACL: true` repo call in this codebase (all transitively scoped to the caller's own
|
|
23
|
+
* mailbox via a session FID/MID registry that itself only ever gets populated from mailbox-scoped queries),
|
|
24
|
+
* `calendarEventRepo.find({ icalUid })` below deliberately searches *every* mailbox on this server - the
|
|
25
|
+
* `CalendarEvent` being responded to belongs to the meeting's *organizer*, who is very often a different
|
|
26
|
+
* mailbox than the attendee submitting this response, and the caller's own session has no way to know which
|
|
27
|
+
* mailbox that is. Safety instead comes from two independent properties: `icalUid` is a server-generated
|
|
28
|
+
* `crypto.randomUUID()` (see `RopSaveChangesMessageHandler.saveAppointment`), for all practical purposes
|
|
29
|
+
* globally unique, so this can't be used to enumerate or collide with another organizer's events; and the
|
|
30
|
+
* actual authorization gate is the attendee-membership check a few lines down (`callerAddresses.has(...)`),
|
|
31
|
+
* derived entirely from *this* caller's own resolved mailbox record, not from anything the client supplied -
|
|
32
|
+
* a crafted `PidLidGlobalObjectId` can at best name a real `icalUid` it doesn't already know the value of
|
|
33
|
+
* (astronomically unlikely against a random UUID) and still can't mutate an event unless the caller's own
|
|
34
|
+
* mailbox is genuinely a listed attendee on it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function submitMeetingResponse(messageClass: string, draftProperties: Record<string, string>, context: RopContext): Promise<void>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RepoUtils } from "@rapidrest/service-core";
|
|
2
|
+
import { type BlobStore } from "@rapidmx/restapi";
|
|
3
|
+
/** `PidTagBody` (`[MS-OXPROPS]`): property ID `0x1000`, `PtypString`. The only streamable property this
|
|
4
|
+
* pragmatic subset supports via `RopOpenStream`/`RopReadStream` - `PidTagHtml`/`PidTagRtfCompressed` are a
|
|
5
|
+
* documented gap (see `RopOpenStreamHandler`'s own doc comment). */
|
|
6
|
+
export declare const PID_TAG_BODY = 4096;
|
|
7
|
+
/**
|
|
8
|
+
* Resolves a `"message:<uid>"` target's plain-text body as the exact byte sequence `RopReadStream` must serve
|
|
9
|
+
* for a streamed `PtypString` property - confirmed via `[MS-OXCPRPT]`'s `RopOpenStream` page ("a string of
|
|
10
|
+
* Unicode characters in UTF-16LE format encoding with terminating null character"), the identical encoding
|
|
11
|
+
* `writePropertyValue()` already uses for an inline `PtypString` value, so this reuses
|
|
12
|
+
* `BufferWriter.writeNullTerminatedUtf16LE` rather than reimplementing it.
|
|
13
|
+
*
|
|
14
|
+
* Always derived from the message's raw MIME source (`bodyBlobKey`, via `mailparser` - the same fallback path
|
|
15
|
+
* `ItemOperationsCommand.fetchMessage()` already uses for EAS's own plain-text body delivery) rather than
|
|
16
|
+
* `sanitizedHtmlBlobKey` - `PidTagBody` is specifically the plain-text body per `[MS-OXPROPS]`, unlike EAS's
|
|
17
|
+
* `Body` element, which can carry either format tagged by its own `Type` field.
|
|
18
|
+
*
|
|
19
|
+
* Recomputed on every call rather than cached anywhere (including on the `"stream"` handle itself) -
|
|
20
|
+
* `MapiSessionContext` is serialized through `RedisCache`'s JSON round trip for multi-instance deployments, so
|
|
21
|
+
* a raw `Buffer` stored there wouldn't survive it (the same class of gap this codebase already documented for
|
|
22
|
+
* `Date` fields, see `MapiSessionContext`'s own doc comment). A documented, pragmatic trade-off: a large body
|
|
23
|
+
* read across several `RopReadStream` calls re-parses the MIME source each time rather than once - correct,
|
|
24
|
+
* not byte-perfect-efficient.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveMessageBodyBytes(target: string, messageRepo: RepoUtils<any>, blobStore: BlobStore): Promise<Buffer>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { RepoUtils } from "@rapidrest/service-core";
|
|
2
|
+
import type { MapiSessionContext } from "../MapiSessionManager.js";
|
|
3
|
+
/**
|
|
4
|
+
* The `RopGetContentsTable` analog of `FolderTarget.ts`: resolves a `"message:<uid>"` row target (see
|
|
5
|
+
* `RopGetContentsTableHandler`'s own doc comment for why contents-table rows use this prefix instead of
|
|
6
|
+
* `FolderTarget.ts`'s `"folder:"`/`"virtual:"`) into the display data a message-table row needs.
|
|
7
|
+
*/
|
|
8
|
+
export interface MessageTargetInfo {
|
|
9
|
+
subject: string;
|
|
10
|
+
read: boolean;
|
|
11
|
+
hasAttachments: boolean;
|
|
12
|
+
receivedDate: Date;
|
|
13
|
+
/** `Label.uid`s applied to this message (Gmail-style, independent of folder placement) - resolved to
|
|
14
|
+
* display names for `PidNameKeywords` (Outlook Categories) by `PropertyResolvers.ts`, which is where the
|
|
15
|
+
* `labelRepo` needed to translate a uid into a name lives. */
|
|
16
|
+
labelUids: string[];
|
|
17
|
+
}
|
|
18
|
+
/** Degrades to empty-looking values for a `"message:<uid>"` target whose real `Message` has since vanished
|
|
19
|
+
* (soft-deleted or otherwise), the same "don't fail the whole ROP over one stale row" principle
|
|
20
|
+
* `FolderTarget.resolveFolderInfo` already applies. */
|
|
21
|
+
export declare function resolveMessageInfo(target: string, messageRepo: RepoUtils<any>): Promise<MessageTargetInfo>;
|
|
22
|
+
/** Resolves the messages directly in `folderUid`, as `"message:<uid>"` target strings, for
|
|
23
|
+
* `RopGetContentsTable`. */
|
|
24
|
+
export declare function resolveFolderMessages(folderUid: string, messageRepo: RepoUtils<any>): Promise<string[]>;
|
|
25
|
+
/**
|
|
26
|
+
* Returns `target`'s existing MID if an earlier `RopQueryRows` row already assigned one, otherwise assigns and
|
|
27
|
+
* remembers the next free small integer MID. The exact `FolderTarget.assignOrGetFid` pattern, adapted for
|
|
28
|
+
* messages: this is what lets a client `RopOpenMessage` a message it only ever learned about via a
|
|
29
|
+
* `RopQueryRows` row's `PidTagMid` column.
|
|
30
|
+
*
|
|
31
|
+
* Backed by `session.messageTargetIds` (target -> MID) and `session.nextMessageId`, an O(1) reverse index/
|
|
32
|
+
* counter pair rather than a linear scan of `session.messageIds` plus a `Math.max(...spread)` over its keys -
|
|
33
|
+
* see `FolderTarget.assignOrGetFid`'s own doc comment for why both of those were real costs (not just
|
|
34
|
+
* theoretical ones) at real mailbox/session scale: `messageIds` only ever grows for a session's lifetime as a
|
|
35
|
+
* client pages through a mailbox, so a linear-scan lookup repeated once per row is quadratic over a session
|
|
36
|
+
* that pages through many messages.
|
|
37
|
+
*/
|
|
38
|
+
export declare function assignOrGetMid(session: MapiSessionContext, target: string): number;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { MapiSessionContext } from "../MapiSessionManager.js";
|
|
2
|
+
/** A `PropertyName` (`[MS-OXCDATA]` §2.6.1): identifies a named property by a property-set `guid` plus either a
|
|
3
|
+
* numeric `lid` (`kind: "lid"`) or a string `name` (`kind: "name"`) - never both. `guid` is the textual form
|
|
4
|
+
* `MapiGuid.ts`'s `encodeGuid`/`decodeGuid` already produce/consume (the wire `PropertyName` structure's own
|
|
5
|
+
* `GUID` field is a `FlatUID` - confirmed, per its own spec page, to be byte-identical to the same little-endian
|
|
6
|
+
* `Data1`/`Data2`/`Data3`+as-is-`Data4` layout `MapiGuid.ts` already implements, so no second GUID codec is
|
|
7
|
+
* needed here). */
|
|
8
|
+
export interface PropertyName {
|
|
9
|
+
guid: string;
|
|
10
|
+
kind: "lid" | "name";
|
|
11
|
+
lid?: number;
|
|
12
|
+
name?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Returns `propertyName`'s existing numeric property ID if an earlier `RopGetPropertyIdsFromNames` call in
|
|
16
|
+
* this session already assigned one, otherwise assigns and remembers the next free ID starting at
|
|
17
|
+
* `FIRST_NAMED_PROPERTY_ID`. This pragmatic subset always behaves as if the request's own `Flags` field
|
|
18
|
+
* requested "assign a new ID if unmapped" (`0x02`) - real Exchange's alternative (`0x00`, "only return
|
|
19
|
+
* already-mapped IDs") exists to let a client probe without committing a mailbox-wide registration; since this
|
|
20
|
+
* registry is already only ever session-scoped (not a real persisted per-mailbox mapping table), there is no
|
|
21
|
+
* meaningful difference between "probe" and "assign" here.
|
|
22
|
+
*
|
|
23
|
+
* Backed by `session.namedProperties`/`namedPropertyIds` (a forward and reverse map, kept in sync) and
|
|
24
|
+
* `session.nextNamedPropertyId`, an O(1) lookup/assignment pair rather than a linear scan plus a
|
|
25
|
+
* `Math.max(...spread)` over `Object.values(session.namedProperties)` - both real costs at scale: the spread
|
|
26
|
+
* form risks a `RangeError: Maximum call stack size exceeded` once a session has registered enough distinct
|
|
27
|
+
* names to exceed V8's function-argument-count limit, and the registry only ever grows for a session's
|
|
28
|
+
* lifetime, so a linear scan repeated once per property per row is quadratic over a session that resolves many
|
|
29
|
+
* named properties.
|
|
30
|
+
*
|
|
31
|
+
* Once `nextNamedPropertyId` would exceed `LAST_NAMED_PROPERTY_ID` (32,768 distinct names already registered
|
|
32
|
+
* this session - the entire `0x8000`-`0xFFFF` numeric ID space this pragmatic subset has to hand out), a *new*
|
|
33
|
+
* name can no longer be assigned a real ID; this returns `0x0000` for it instead of throwing, the exact value
|
|
34
|
+
* `[MS-OXCPRPT]`'s own `RopGetPropertyIdsFromNames` processing rules already use for "this `PropertyName`
|
|
35
|
+
* could not be resolved" (the same value this pragmatic subset already produces for a `Kind = 0xFF` entry) -
|
|
36
|
+
* not a new failure mode, just the same one applied to a different unmappable case. A name already registered
|
|
37
|
+
* before the registry filled up keeps returning its real, previously-assigned ID.
|
|
38
|
+
*/
|
|
39
|
+
export declare function assignOrGetNamedPropertyId(session: MapiSessionContext, propertyName: PropertyName): number;
|
|
40
|
+
/** The reverse lookup `RopSetProperties`/`RopGetPropertiesSpecific`/`RopQueryRows` use to recognize an incoming
|
|
41
|
+
* property ID `>= 0x8000` as one of this session's own mapped named properties - an O(1) lookup against
|
|
42
|
+
* `session.namedPropertyIds` (kept in sync by `assignOrGetNamedPropertyId`) rather than a linear scan of
|
|
43
|
+
* `session.namedProperties` with a `JSON.parse` per candidate - see that function's own doc comment for why
|
|
44
|
+
* this matters at scale, particularly since this is called once per named-property column per row from
|
|
45
|
+
* `RopQueryRows`. */
|
|
46
|
+
export declare function resolveNamedProperty(session: MapiSessionContext, propertyId: number): PropertyName | undefined;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { PropertyType, PropertyValueData } from "../codec/PropertyValue.js";
|
|
2
|
+
import type { MapiSessionContext } from "../MapiSessionManager.js";
|
|
3
|
+
import { CalendarEventTargetInfo } from "./CalendarEventTarget.js";
|
|
4
|
+
import { ContactTargetInfo } from "./ContactTarget.js";
|
|
5
|
+
import { FolderResolutionCache, FolderTargetInfo } from "./FolderTarget.js";
|
|
6
|
+
import { MessageTargetInfo } from "./MessageTarget.js";
|
|
7
|
+
import type { RopContext } from "./RopHandler.js";
|
|
8
|
+
import { TaskTargetInfo } from "./TaskTarget.js";
|
|
9
|
+
/** A type-appropriate zero/empty value for a requested property this handler has no real data for - keeps
|
|
10
|
+
* `StandardPropertyRow` encoding valid (a real value of the right type, per `writePropertyValue`'s
|
|
11
|
+
* expectations) without needing to model every property a client could ever ask for. Shared by
|
|
12
|
+
* `RopQueryRowsHandler` (table rows) and `RopGetPropertiesSpecificHandler` (single-object property fetch). */
|
|
13
|
+
export declare function defaultValueForType(propertyType: PropertyType): PropertyValueData;
|
|
14
|
+
/** Resolves one requested property's value for a `"folder:<uid>"`/`"virtual:<name>"` target. */
|
|
15
|
+
export declare function folderValueFor(session: MapiSessionContext, propertyId: number, propertyType: PropertyType, target: string, info: FolderTargetInfo): PropertyValueData;
|
|
16
|
+
/** Resolves one requested property's value for a `"message:<uid>"` target. `labelNames` is this message's own
|
|
17
|
+
* `labelUids` already resolved to display names (see `resolvePropertyValues`'s own doc comment) - a plain
|
|
18
|
+
* parameter rather than a repo lookup here, since this function (like every other `xxxValueFor`) is
|
|
19
|
+
* synchronous and can't do its own async DB access. */
|
|
20
|
+
export declare function messageValueFor(session: MapiSessionContext, propertyId: number, propertyType: PropertyType, target: string, info: MessageTargetInfo, labelNames?: string[]): PropertyValueData;
|
|
21
|
+
/** Resolves one requested property's value for a `"calendarEvent:<uid>"` target. Almost every Appointment
|
|
22
|
+
* property is a *named* property (`PidLid*`, no fixed numeric ID) rather than a plain `PidTag*` - a property ID
|
|
23
|
+
* `>= 0x8000` is resolved back to its `(PropertySet GUID, LID)` identity via `resolveNamedProperty` before this
|
|
24
|
+
* can dispatch on it (see `NamedPropertyRegistry.ts`'s own doc comment and the architecture plan's "Calendar
|
|
25
|
+
* support" section for the full named-property table this switch implements). */
|
|
26
|
+
export declare function calendarEventValueFor(session: MapiSessionContext, propertyId: number, propertyType: PropertyType, target: string, info: CalendarEventTargetInfo, callerAddress: string): PropertyValueData;
|
|
27
|
+
/** Resolves one requested property's value for a `"contact:<uid>"` target - a `Folder` of type `CONTACTS`'s
|
|
28
|
+
* content-table rows/opened items. Every field here is a plain fixed `PidTag` (`[MS-OXOCNTC]`), unlike Calendar/
|
|
29
|
+
* Task's named-property tables, since Contact's own well-known fields all have real numeric IDs. */
|
|
30
|
+
export declare function contactValueFor(session: MapiSessionContext, propertyId: number, propertyType: PropertyType, target: string, info: ContactTargetInfo): PropertyValueData;
|
|
31
|
+
/** Resolves one requested property's value for a `"task:<uid>"` target - a `Folder` of type `TASKS`'s
|
|
32
|
+
* content-table rows/opened items. Almost every Task-specific property is a *named* property (`PidLid*`) under
|
|
33
|
+
* `PSETID_Task`, resolved back to its `(PropertySet GUID, LID)` identity via `resolveNamedProperty` exactly the
|
|
34
|
+
* same way `calendarEventValueFor` handles `PSETID_Appointment`/`PSETID_Common` - see `TaskNamedProperties.ts`'s
|
|
35
|
+
* own doc comment for the LID table this switch implements. */
|
|
36
|
+
export declare function taskValueFor(session: MapiSessionContext, propertyId: number, propertyType: PropertyType, target: string, info: TaskTargetInfo): PropertyValueData;
|
|
37
|
+
/** An optional, purely request-scoped (never persisted) memo shared across every `resolvePropertyValues` call
|
|
38
|
+
* within one ROP handler invocation - e.g. one `RopQueryRows` call resolving many table rows, or the (rarer)
|
|
39
|
+
* case of `RopGetPropertiesSpecific` resolving many columns for one object. Without it, both `allFolders` (via
|
|
40
|
+
* `resolveFolderInfo`'s own `hasChildren` computation) and `callerAddress` (the calendar organizer/attendee
|
|
41
|
+
* lookup below) would be re-fetched from the repo on every single row even though they can only ever have one
|
|
42
|
+
* value for the whole batch (this mailbox's folder list, this mailbox's own primary address) - a real,
|
|
43
|
+
* measurable cost for a hierarchy/calendar table with many rows, not just a theoretical one. A caller that
|
|
44
|
+
* resolves only a single target (`RopGetPropertiesSpecificHandler`) can simply omit this - resolving without a
|
|
45
|
+
* cache is still correct, just not deduplicated across calls it doesn't make anyway. */
|
|
46
|
+
export interface ResolutionCache extends FolderResolutionCache {
|
|
47
|
+
callerAddress?: string;
|
|
48
|
+
/** `Label.uid` -> `Label.name`, for the whole mailbox - built at most once per call the same way
|
|
49
|
+
* `allFolders`/`callerAddress` are, the first time a message row actually has a non-empty `labelUids` to
|
|
50
|
+
* resolve (most messages have none, so this stays unbuilt for a page with no labeled messages at all). */
|
|
51
|
+
labelNamesByUid?: Map<string, string>;
|
|
52
|
+
}
|
|
53
|
+
/** Resolves every column in `columns` for a single `target` (a `"folder:"`/`"virtual:"`/`"message:"`/
|
|
54
|
+
* `"calendarEvent:"`/`"contact:"`/`"task:"` target string), in order - the shared implementation behind both
|
|
55
|
+
* `RopQueryRowsHandler` (one call per table row) and `RopGetPropertiesSpecificHandler` (one call for the single
|
|
56
|
+
* object a handle refers to). */
|
|
57
|
+
export declare function resolvePropertyValues(target: string, columns: {
|
|
58
|
+
propertyId: number;
|
|
59
|
+
propertyType: PropertyType;
|
|
60
|
+
}[], context: Pick<RopContext, "mailboxUid" | "session" | "folderRepo" | "messageRepo" | "calendarEventRepo" | "mailboxRepo" | "contactRepo" | "taskRepo" | "labelRepo">, cache?: ResolutionCache): Promise<PropertyValueData[]>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { BufferReader, BufferWriter } from "../codec/BufferCursor.js";
|
|
2
|
+
import type { RopContext, RopHandler } from "./RopHandler.js";
|
|
3
|
+
/**
|
|
4
|
+
* `RopCreateMessage` (`[MS-OXCMSG]`/`[MS-OXCROPS]`): begins composing a new message in a folder, producing a
|
|
5
|
+
* new `"message"` Server object handle later ROPs (`RopSetProperties`, `RopOpenStream`, `RopSaveChangesMessage`)
|
|
6
|
+
* reference by `InputHandleIndex`. Unlike `RopOpenMessage`'s handle (an already-real, already-saved message),
|
|
7
|
+
* this handle's `entityUid` is deliberately left `""` - there is no real backing `Message` row, and won't be
|
|
8
|
+
* one until `RopSubmitMessage` actually sends it (this pragmatic subset has no separate Drafts-folder
|
|
9
|
+
* persistence step; see `RopSaveChangesMessageHandler`'s own doc comment). `draftFolderUid` remembers the
|
|
10
|
+
* target folder for that eventual send, and `draftProperties` starts empty, filled in by later
|
|
11
|
+
* `RopSetProperties`/`RopWriteStream` calls.
|
|
12
|
+
*
|
|
13
|
+
* `CodePageId` is decoded to advance past it correctly but not otherwise acted on (this pragmatic subset always
|
|
14
|
+
* encodes strings as UTF-16LE/UTF-8 directly, the same treatment `RopOpenMessageHandler` gives it).
|
|
15
|
+
* `AssociatedFlag` (FAI messages - hidden configuration/rules objects, not real mail) is decoded but not
|
|
16
|
+
* honored - this pragmatic subset's compose/send path only ever produces ordinary mail messages.
|
|
17
|
+
*
|
|
18
|
+
* @author Jean-Philippe Steinmetz
|
|
19
|
+
*/
|
|
20
|
+
export declare class RopCreateMessageHandler implements RopHandler {
|
|
21
|
+
readonly ropId = 6;
|
|
22
|
+
handle(reader: BufferReader, writer: BufferWriter, context: RopContext): void;
|
|
23
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { BufferReader, BufferWriter } from "../codec/BufferCursor.js";
|
|
2
|
+
import type { RopContext, RopHandler } from "./RopHandler.js";
|
|
3
|
+
/**
|
|
4
|
+
* `RopDeleteFolder` (`[MS-OXCFOLD]`/`[MS-OXCROPS]`, RopId `0x1D`): deletes a folder by `FolderId`. Reuses
|
|
5
|
+
* `RecoverableRepoUtils.delete()` exactly as-built in Phase 2 for `Folder`/`Message`/`CalendarEvent` - see
|
|
6
|
+
* `RopDeleteMessagesHandler.ts`'s own doc comment for why that matters (EAS's watermark-based incremental
|
|
7
|
+
* delete detection depends on it).
|
|
8
|
+
*
|
|
9
|
+
* **Real spec semantics honored, not simplified away**: per `[MS-OXCFOLD]`, `RopDeleteFolder` only operates on
|
|
10
|
+
* an empty folder by default - `DEL_MESSAGES` must be set to also delete the folder's own messages/calendar
|
|
11
|
+
* events/contacts/tasks, and `DEL_FOLDERS` to also delete (recursively) its subfolders; a non-empty folder
|
|
12
|
+
* deleted without the matching flag fails rather than silently cascading. `DELETE_HARD_DELETE` maps directly
|
|
13
|
+
* onto `RepoDeleteOptions.purge` - real, exact semantic overlap with this library's own soft-delete model, not
|
|
14
|
+
* a coincidence this codec papers over.
|
|
15
|
+
*
|
|
16
|
+
* **Contacts/Tasks folders**: emptiness is checked against `contactRepo`/`taskRepo` alongside `messageRepo`/
|
|
17
|
+
* `calendarEventRepo`, and both are included in the delete cascade - a `CONTACTS`/`TASKS` folder's real content
|
|
18
|
+
* lives in those repos (see `ContactTarget.ts`/`TaskTarget.ts`), not `messageRepo`, so checking only the latter
|
|
19
|
+
* would let a non-empty Contacts/Tasks folder be deleted without `DEL_MESSAGES` and orphan its rows. Guarded
|
|
20
|
+
* with `context.contactRepo`/`taskRepo` presence checks the same way `RopGetContentsTableHandler` does, since
|
|
21
|
+
* `RopContext`'s doc comment documents both as optional.
|
|
22
|
+
*
|
|
23
|
+
* The request's own `InputHandleIndex` (nominally the *parent* folder of the one being deleted) is validated to
|
|
24
|
+
* be a real, open folder handle but not cross-checked against the target folder's actual `parentFolderUid` -
|
|
25
|
+
* the same pragmatic simplification `RopOpenMessageHandler`'s own doc comment documents for its analogous
|
|
26
|
+
* `FolderId` field.
|
|
27
|
+
*
|
|
28
|
+
* @author Jean-Philippe Steinmetz
|
|
29
|
+
*/
|
|
30
|
+
export declare class RopDeleteFolderHandler implements RopHandler {
|
|
31
|
+
readonly ropId = 29;
|
|
32
|
+
handle(reader: BufferReader, writer: BufferWriter, context: RopContext): Promise<void>;
|
|
33
|
+
/** Cascades a `DEL_FOLDERS` delete into `folderUid`'s own messages/calendar events/subfolders before
|
|
34
|
+
* deleting it - this data model's folder hierarchy is a plain parent-pointer tree (never a graph), so
|
|
35
|
+
* unbounded recursion here needs no cycle protection, the same assumption `FolderTarget.resolveFolderChildren`
|
|
36
|
+
* already makes. */
|
|
37
|
+
private deleteFolderRecursive;
|
|
38
|
+
}
|