@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.
Files changed (180) hide show
  1. package/LICENSE +382 -0
  2. package/README.md +60 -0
  3. package/dist/lib/BaseMapiEmsmdbRoute.js +297 -0
  4. package/dist/lib/BaseMapiEmsmdbRoute.js.map +1 -0
  5. package/dist/lib/BaseMapiNspiRoute.js +114 -0
  6. package/dist/lib/BaseMapiNspiRoute.js.map +1 -0
  7. package/dist/lib/MapiSessionManager.js +119 -0
  8. package/dist/lib/MapiSessionManager.js.map +1 -0
  9. package/dist/lib/RopDispatcher.js +34 -0
  10. package/dist/lib/RopDispatcher.js.map +1 -0
  11. package/dist/lib/codec/AppointmentRecurrence.js +314 -0
  12. package/dist/lib/codec/AppointmentRecurrence.js.map +1 -0
  13. package/dist/lib/codec/BufferCursor.js +213 -0
  14. package/dist/lib/codec/BufferCursor.js.map +1 -0
  15. package/dist/lib/codec/GlobalObjectId.js +92 -0
  16. package/dist/lib/codec/GlobalObjectId.js.map +1 -0
  17. package/dist/lib/codec/MapiGuid.js +41 -0
  18. package/dist/lib/codec/MapiGuid.js.map +1 -0
  19. package/dist/lib/codec/MapiTimeZone.js +93 -0
  20. package/dist/lib/codec/MapiTimeZone.js.map +1 -0
  21. package/dist/lib/codec/PropertyValue.js +214 -0
  22. package/dist/lib/codec/PropertyValue.js.map +1 -0
  23. package/dist/lib/codec/RopBuffer.js +26 -0
  24. package/dist/lib/codec/RopBuffer.js.map +1 -0
  25. package/dist/lib/codec/TypedString.js +54 -0
  26. package/dist/lib/codec/TypedString.js.map +1 -0
  27. package/dist/lib/index.js +99 -0
  28. package/dist/lib/index.js.map +1 -0
  29. package/dist/lib/mongo/MapiEmsmdbRouteMongo.js +84 -0
  30. package/dist/lib/mongo/MapiEmsmdbRouteMongo.js.map +1 -0
  31. package/dist/lib/mongo/MapiNspiRouteMongo.js +33 -0
  32. package/dist/lib/mongo/MapiNspiRouteMongo.js.map +1 -0
  33. package/dist/lib/mongo/index.js +7 -0
  34. package/dist/lib/mongo/index.js.map +1 -0
  35. package/dist/lib/mongo.js +6 -0
  36. package/dist/lib/mongo.js.map +1 -0
  37. package/dist/lib/nspi/NspiBindHandler.js +54 -0
  38. package/dist/lib/nspi/NspiBindHandler.js.map +1 -0
  39. package/dist/lib/nspi/NspiCodec.js +106 -0
  40. package/dist/lib/nspi/NspiCodec.js.map +1 -0
  41. package/dist/lib/nspi/NspiGetMatchesHandler.js +142 -0
  42. package/dist/lib/nspi/NspiGetMatchesHandler.js.map +1 -0
  43. package/dist/lib/rop/CalendarEventTarget.js +27 -0
  44. package/dist/lib/rop/CalendarEventTarget.js.map +1 -0
  45. package/dist/lib/rop/CalendarNamedProperties.js +57 -0
  46. package/dist/lib/rop/CalendarNamedProperties.js.map +1 -0
  47. package/dist/lib/rop/ContactTarget.js +23 -0
  48. package/dist/lib/rop/ContactTarget.js.map +1 -0
  49. package/dist/lib/rop/FastTransferStream.js +98 -0
  50. package/dist/lib/rop/FastTransferStream.js.map +1 -0
  51. package/dist/lib/rop/FolderTarget.js +111 -0
  52. package/dist/lib/rop/FolderTarget.js.map +1 -0
  53. package/dist/lib/rop/MeetingMessageClassHandler.js +95 -0
  54. package/dist/lib/rop/MeetingMessageClassHandler.js.map +1 -0
  55. package/dist/lib/rop/MessageBodyStream.js +42 -0
  56. package/dist/lib/rop/MessageBodyStream.js.map +1 -0
  57. package/dist/lib/rop/MessageTarget.js +44 -0
  58. package/dist/lib/rop/MessageTarget.js.map +1 -0
  59. package/dist/lib/rop/NamedPropertyRegistry.js +66 -0
  60. package/dist/lib/rop/NamedPropertyRegistry.js.map +1 -0
  61. package/dist/lib/rop/PropertyResolvers.js +313 -0
  62. package/dist/lib/rop/PropertyResolvers.js.map +1 -0
  63. package/dist/lib/rop/RopCreateMessageHandler.js +53 -0
  64. package/dist/lib/rop/RopCreateMessageHandler.js.map +1 -0
  65. package/dist/lib/rop/RopDeleteFolderHandler.js +133 -0
  66. package/dist/lib/rop/RopDeleteFolderHandler.js.map +1 -0
  67. package/dist/lib/rop/RopDeleteMessagesHandler.js +70 -0
  68. package/dist/lib/rop/RopDeleteMessagesHandler.js.map +1 -0
  69. package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js +61 -0
  70. package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js.map +1 -0
  71. package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js +61 -0
  72. package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js.map +1 -0
  73. package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js +75 -0
  74. package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js.map +1 -0
  75. package/dist/lib/rop/RopGetContentsTableHandler.js +76 -0
  76. package/dist/lib/rop/RopGetContentsTableHandler.js.map +1 -0
  77. package/dist/lib/rop/RopGetHierarchyTableHandler.js +40 -0
  78. package/dist/lib/rop/RopGetHierarchyTableHandler.js.map +1 -0
  79. package/dist/lib/rop/RopGetPropertiesSpecificHandler.js +55 -0
  80. package/dist/lib/rop/RopGetPropertiesSpecificHandler.js.map +1 -0
  81. package/dist/lib/rop/RopGetPropertyIdsFromNamesHandler.js +92 -0
  82. package/dist/lib/rop/RopGetPropertyIdsFromNamesHandler.js.map +1 -0
  83. package/dist/lib/rop/RopHandler.js +2 -0
  84. package/dist/lib/rop/RopHandler.js.map +1 -0
  85. package/dist/lib/rop/RopLogonHandler.js +132 -0
  86. package/dist/lib/rop/RopLogonHandler.js.map +1 -0
  87. package/dist/lib/rop/RopOpenFolderHandler.js +46 -0
  88. package/dist/lib/rop/RopOpenFolderHandler.js.map +1 -0
  89. package/dist/lib/rop/RopOpenMessageHandler.js +81 -0
  90. package/dist/lib/rop/RopOpenMessageHandler.js.map +1 -0
  91. package/dist/lib/rop/RopOpenStreamHandler.js +81 -0
  92. package/dist/lib/rop/RopOpenStreamHandler.js.map +1 -0
  93. package/dist/lib/rop/RopQueryRowsHandler.js +91 -0
  94. package/dist/lib/rop/RopQueryRowsHandler.js.map +1 -0
  95. package/dist/lib/rop/RopReadStreamHandler.js +55 -0
  96. package/dist/lib/rop/RopReadStreamHandler.js.map +1 -0
  97. package/dist/lib/rop/RopReleaseHandler.js +20 -0
  98. package/dist/lib/rop/RopReleaseHandler.js.map +1 -0
  99. package/dist/lib/rop/RopSaveChangesMessageHandler.js +210 -0
  100. package/dist/lib/rop/RopSaveChangesMessageHandler.js.map +1 -0
  101. package/dist/lib/rop/RopSetColumnsHandler.js +52 -0
  102. package/dist/lib/rop/RopSetColumnsHandler.js.map +1 -0
  103. package/dist/lib/rop/RopSetPropertiesHandler.js +162 -0
  104. package/dist/lib/rop/RopSetPropertiesHandler.js.map +1 -0
  105. package/dist/lib/rop/RopSubmitMessageHandler.js +314 -0
  106. package/dist/lib/rop/RopSubmitMessageHandler.js.map +1 -0
  107. package/dist/lib/rop/RopWriteStreamHandler.js +45 -0
  108. package/dist/lib/rop/RopWriteStreamHandler.js.map +1 -0
  109. package/dist/lib/rop/TaskNamedProperties.js +29 -0
  110. package/dist/lib/rop/TaskNamedProperties.js.map +1 -0
  111. package/dist/lib/rop/TaskTarget.js +19 -0
  112. package/dist/lib/rop/TaskTarget.js.map +1 -0
  113. package/dist/lib/sql/MapiEmsmdbRouteSQL.js +84 -0
  114. package/dist/lib/sql/MapiEmsmdbRouteSQL.js.map +1 -0
  115. package/dist/lib/sql/MapiNspiRouteSQL.js +33 -0
  116. package/dist/lib/sql/MapiNspiRouteSQL.js.map +1 -0
  117. package/dist/lib/sql/index.js +7 -0
  118. package/dist/lib/sql/index.js.map +1 -0
  119. package/dist/lib/sql.js +6 -0
  120. package/dist/lib/sql.js.map +1 -0
  121. package/dist/types/BaseMapiEmsmdbRoute.d.ts +82 -0
  122. package/dist/types/BaseMapiNspiRoute.d.ts +38 -0
  123. package/dist/types/MapiSessionManager.d.ts +130 -0
  124. package/dist/types/RopDispatcher.d.ts +17 -0
  125. package/dist/types/codec/AppointmentRecurrence.d.ts +14 -0
  126. package/dist/types/codec/BufferCursor.d.ts +78 -0
  127. package/dist/types/codec/GlobalObjectId.d.ts +11 -0
  128. package/dist/types/codec/MapiGuid.d.ts +6 -0
  129. package/dist/types/codec/MapiTimeZone.d.ts +7 -0
  130. package/dist/types/codec/PropertyValue.d.ts +77 -0
  131. package/dist/types/codec/RopBuffer.d.ts +25 -0
  132. package/dist/types/codec/TypedString.d.ts +17 -0
  133. package/dist/types/index.d.ts +94 -0
  134. package/dist/types/mongo/MapiEmsmdbRouteMongo.d.ts +19 -0
  135. package/dist/types/mongo/MapiNspiRouteMongo.d.ts +13 -0
  136. package/dist/types/mongo/index.d.ts +2 -0
  137. package/dist/types/mongo.d.ts +1 -0
  138. package/dist/types/nspi/NspiBindHandler.d.ts +22 -0
  139. package/dist/types/nspi/NspiCodec.d.ts +59 -0
  140. package/dist/types/nspi/NspiGetMatchesHandler.d.ts +24 -0
  141. package/dist/types/rop/CalendarEventTarget.d.ts +29 -0
  142. package/dist/types/rop/CalendarNamedProperties.d.ts +37 -0
  143. package/dist/types/rop/ContactTarget.d.ts +29 -0
  144. package/dist/types/rop/FastTransferStream.d.ts +18 -0
  145. package/dist/types/rop/FolderTarget.d.ts +63 -0
  146. package/dist/types/rop/MeetingMessageClassHandler.d.ts +36 -0
  147. package/dist/types/rop/MessageBodyStream.d.ts +26 -0
  148. package/dist/types/rop/MessageTarget.d.ts +38 -0
  149. package/dist/types/rop/NamedPropertyRegistry.d.ts +46 -0
  150. package/dist/types/rop/PropertyResolvers.d.ts +60 -0
  151. package/dist/types/rop/RopCreateMessageHandler.d.ts +23 -0
  152. package/dist/types/rop/RopDeleteFolderHandler.d.ts +38 -0
  153. package/dist/types/rop/RopDeleteMessagesHandler.d.ts +31 -0
  154. package/dist/types/rop/RopFastTransferSourceCopyPropertiesHandler.d.ts +24 -0
  155. package/dist/types/rop/RopFastTransferSourceCopyToHandler.d.ts +24 -0
  156. package/dist/types/rop/RopFastTransferSourceGetBufferHandler.d.ts +23 -0
  157. package/dist/types/rop/RopGetContentsTableHandler.d.ts +31 -0
  158. package/dist/types/rop/RopGetHierarchyTableHandler.d.ts +17 -0
  159. package/dist/types/rop/RopGetPropertiesSpecificHandler.d.ts +23 -0
  160. package/dist/types/rop/RopGetPropertyIdsFromNamesHandler.d.ts +23 -0
  161. package/dist/types/rop/RopHandler.d.ts +67 -0
  162. package/dist/types/rop/RopLogonHandler.d.ts +31 -0
  163. package/dist/types/rop/RopOpenFolderHandler.d.ts +19 -0
  164. package/dist/types/rop/RopOpenMessageHandler.d.ts +27 -0
  165. package/dist/types/rop/RopOpenStreamHandler.d.ts +27 -0
  166. package/dist/types/rop/RopQueryRowsHandler.d.ts +36 -0
  167. package/dist/types/rop/RopReadStreamHandler.d.ts +18 -0
  168. package/dist/types/rop/RopReleaseHandler.d.ts +14 -0
  169. package/dist/types/rop/RopSaveChangesMessageHandler.d.ts +44 -0
  170. package/dist/types/rop/RopSetColumnsHandler.d.ts +21 -0
  171. package/dist/types/rop/RopSetPropertiesHandler.d.ts +38 -0
  172. package/dist/types/rop/RopSubmitMessageHandler.d.ts +70 -0
  173. package/dist/types/rop/RopWriteStreamHandler.d.ts +21 -0
  174. package/dist/types/rop/TaskNamedProperties.d.ts +24 -0
  175. package/dist/types/rop/TaskTarget.d.ts +24 -0
  176. package/dist/types/sql/MapiEmsmdbRouteSQL.d.ts +19 -0
  177. package/dist/types/sql/MapiNspiRouteSQL.d.ts +13 -0
  178. package/dist/types/sql/index.d.ts +2 -0
  179. package/dist/types/sql.d.ts +1 -0
  180. package/package.json +104 -0
@@ -0,0 +1,132 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { BufferWriter } from "../codec/BufferCursor.js";
6
+ import { encodeGuid } from "../codec/MapiGuid.js";
7
+ import { FolderType } from "@rapidmx/restapi";
8
+ import { assignOrGetFid } from "./FolderTarget.js";
9
+ const ROP_ID_LOGON = 0xfe;
10
+ /** A fixed replica identity shared by every logon in this deployment - legitimate per `[MS-OXCSTOR]`'s own
11
+ * "RopLogon ROP Success Response Buffer for Private Mailbox" section ("If the client did not set
12
+ * USE_PER_MDB_REPLID_MAPPING... this value MUST be identical for all private mailbox logons on the same RPC
13
+ * session"); this pragmatic subset never varies replica identity, so one fixed constant satisfies that. */
14
+ const REPL_ID = 1;
15
+ const REPL_GUID = "00000000-0000-0000-0000-000000000001";
16
+ /**
17
+ * Order matches `[MS-OXCSTOR]` §2.2.1.1.3 ("RopLogon ROP Success Response Buffer for Private Mailbox")'s
18
+ * `FolderIds` list exactly: Root, Deferred Action, Spooler Queue, IPM Subtree, Inbox, Outbox, Sent Items,
19
+ * Deleted Items, Common Views, Schedule, Search, Views, Shortcuts (13 folders total, confirmed against the
20
+ * spec page directly). Folders with a matching `FolderType` resolve to a real `Folder` when one exists for
21
+ * this mailbox; the rest are virtual placeholders. Root/IPM Subtree have no real backing row at all in this
22
+ * data model, matching how EAS's own `FolderSync` already represents "the root" as `ParentId "0"` rather than
23
+ * a real `Folder` - not a new gap this ROP introduces. Deferred Action/Spooler Queue/Common Views/Schedule/
24
+ * Search/Views/Shortcuts are MAPI-internal administrative folders this library's `FolderType` enum has no
25
+ * concept of at all (it models the REST/EAS-relevant folder kinds - Inbox/Sent/Drafts/Deleted/Outbox/Junk/
26
+ * Calendar/Contacts/Tasks/Notes/User - not Exchange's own internal bookkeeping folders). A real client rarely
27
+ * if ever tries to open most of these directly; documented gap, not silently dropped.
28
+ */
29
+ const SPECIAL_FOLDERS = [
30
+ { name: "root" },
31
+ { name: "deferredAction" },
32
+ { name: "spoolerQueue" },
33
+ { name: "ipmSubtree" },
34
+ { name: "inbox", folderType: FolderType.INBOX },
35
+ { name: "outbox", folderType: FolderType.OUTBOX },
36
+ { name: "sentItems", folderType: FolderType.SENT_ITEMS },
37
+ { name: "deletedItems", folderType: FolderType.DELETED_ITEMS },
38
+ { name: "commonViews" },
39
+ { name: "schedule" },
40
+ { name: "search" },
41
+ { name: "views" },
42
+ { name: "shortcuts" },
43
+ ];
44
+ /**
45
+ * `RopLogon` (`[MS-OXCSTOR]`/`[MS-OXCROPS]`): the first ROP of any session, a prerequisite for every other
46
+ * ROP. This pragmatic subset always performs a private-mailbox logon against the mailbox `BaseMapiEmsmdbRoute`
47
+ * already resolved and authorized at `Connect` time - the request's own `Essdn` field is decoded (to advance
48
+ * past it correctly) but never trusted for mailbox identity, the same principle `BaseMapiEmsmdbRoute.Connect`
49
+ * already applies to that request's `UserDn` field. Because mailbox resolution already happened upstream,
50
+ * this handler has no real failure path to model (`ecUnknownUser`/`ecLoginFailure`/... are all deferred).
51
+ *
52
+ * @author Jean-Philippe Steinmetz
53
+ */
54
+ export class RopLogonHandler {
55
+ constructor() {
56
+ this.ropId = ROP_ID_LOGON;
57
+ }
58
+ async handle(reader, writer, context) {
59
+ reader.readUInt8(); // LogonId - this pragmatic subset doesn't track multiple concurrent logons per session
60
+ const outputHandleIndex = reader.readUInt8();
61
+ const logonFlags = reader.readUInt8();
62
+ reader.readUInt32LE(); // OpenFlags - unused; every logon behaves as a plain private-mailbox logon
63
+ reader.readUInt32LE(); // StoreState - per spec, unused and always 0
64
+ const essdnSize = reader.readUInt16LE();
65
+ if (essdnSize > 0) {
66
+ reader.readBytes(essdnSize); // Essdn - never trusted for mailbox identity, see class doc comment
67
+ }
68
+ const fids = await this.assignFolderIds(context);
69
+ context.session.handles[outputHandleIndex] = { type: "logon", entityUid: context.mailboxUid };
70
+ writer.writeUInt8(ROP_ID_LOGON);
71
+ writer.writeUInt8(outputHandleIndex);
72
+ writer.writeUInt32LE(0); // ReturnValue - success
73
+ writer.writeUInt8(logonFlags); // echoed back unchanged, per spec
74
+ for (const folder of SPECIAL_FOLDERS) {
75
+ writer.writeBigUInt64LE(BigInt(fids[folder.name]));
76
+ }
77
+ writer.writeUInt8(0x01); // ResponseFlags - Reserved bit only; no OwnerRight/SendAsRight/OOF claims in this subset
78
+ writer.writeBytes(encodeGuid(context.mailboxUid));
79
+ writer.writeUInt16LE(REPL_ID);
80
+ writer.writeBytes(encodeGuid(REPL_GUID));
81
+ writer.writeBytes(encodeLogonTime(new Date()));
82
+ writer.writeBigUInt64LE(1n); // GwartTime - fixed constant; no real GWART/AD concept in this deployment
83
+ writer.writeUInt32LE(0); // StoreState - unused
84
+ }
85
+ /**
86
+ * Assigns this session's FIDs for the 13 special folders, remembering each one's real-`Folder`-or-virtual-
87
+ * placeholder target in `session.folderIds` so a later `RopOpenFolder` can resolve it back to something
88
+ * real. Delegates to `FolderTarget.assignOrGetFid` - the same "reuse an existing FID for this target if one
89
+ * was already assigned, otherwise mint the next free one" logic `RopGetHierarchyTable`/`RopQueryRows` use
90
+ * for child folders - rather than wholesale-replacing `session.folderIds` with a freshly-numbered map
91
+ * starting back at FID 1. A second `RopLogon` on a live session (spec-legal - a real client can reconnect
92
+ * or re-logon within one session context) previously invalidated every FID a client had already learned
93
+ * for a child folder via `RopGetHierarchyTable`/`RopQueryRows` (all assigned starting at FID 14, since the
94
+ * old code always reset the counter to 1-13 for just these specials) and could even cause FIDs 14+ to be
95
+ * silently re-issued to a *different* folder than the one the client last associated with that number.
96
+ * Reusing `assignOrGetFid` means a re-`RopLogon` is now purely additive: these 13 targets get their
97
+ * existing FIDs back if this is a second logon, and any child-folder FIDs already assigned are left alone.
98
+ */
99
+ async assignFolderIds(context) {
100
+ // The four real-folder-type lookups (Inbox/Outbox/Sent Items/Deleted Items) are independent of each
101
+ // other, so resolve them concurrently rather than one sequential round trip per special folder -
102
+ // FID *assignment* itself still happens afterward, in SPECIAL_FOLDERS' own fixed order, so which FID
103
+ // ends up bound to which target is unaffected by fetch order.
104
+ const targets = await Promise.all(SPECIAL_FOLDERS.map(async (folder) => {
105
+ if (!folder.folderType) {
106
+ return `virtual:${folder.name}`;
107
+ }
108
+ const existing = await context.folderRepo.find({ mailboxUid: context.mailboxUid, type: folder.folderType }, { ignoreACL: true, limit: 1 });
109
+ return existing[0] ? `folder:${existing[0].uid}` : `virtual:${folder.name}`;
110
+ }));
111
+ const fids = {};
112
+ SPECIAL_FOLDERS.forEach((folder, index) => {
113
+ fids[folder.name] = assignOrGetFid(context.session, targets[index]);
114
+ });
115
+ return fids;
116
+ }
117
+ }
118
+ /** `LogonTime` structure (`[MS-OXCROPS]` "LogonTime Structure"): `Seconds`/`Minutes`/`Hour`/`DayOfWeek`
119
+ * (1 byte each, `DayOfWeek` Sunday=0)/`Day`/`Month` (1 byte each, `Month` January=1)/`Year` (2 bytes) - all
120
+ * UTC, confirmed against the spec's own byte-layout table. */
121
+ function encodeLogonTime(date) {
122
+ const writer = new BufferWriter();
123
+ writer.writeUInt8(date.getUTCSeconds());
124
+ writer.writeUInt8(date.getUTCMinutes());
125
+ writer.writeUInt8(date.getUTCHours());
126
+ writer.writeUInt8(date.getUTCDay());
127
+ writer.writeUInt8(date.getUTCDate());
128
+ writer.writeUInt8(date.getUTCMonth() + 1);
129
+ writer.writeUInt16LE(date.getUTCFullYear());
130
+ return writer.toBuffer();
131
+ }
132
+ //# sourceMappingURL=RopLogonHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RopLogonHandler.js","sourceRoot":"","sources":["../../../src/rop/RopLogonHandler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ const ROP_ID_OPEN_FOLDER = 0x02;
2
+ /** The well-known MAPI HRESULT `MAPI_E_NOT_FOUND`, reused as this handler's only failure `ReturnValue` (an
3
+ * unrecognized `FolderId` - one not present in `session.folderIds`, i.e. not something `RopLogon` handed out
4
+ * this session). */
5
+ const ERROR_NOT_FOUND = 0x8004010f;
6
+ /**
7
+ * `RopOpenFolder` (`[MS-OXCFOLD]`/`[MS-OXCROPS]`): opens a folder by FID, producing a new `"folder"` Server
8
+ * object handle later ROPs (`RopGetHierarchyTable`, a future `RopGetContentsTable`) reference by
9
+ * `InputHandleIndex`. The request's own `OpenModeFlags` (`OpenSoftDeleted`, ...) is decoded to advance past
10
+ * it correctly but not acted on - this pragmatic subset has no soft-deleted-folder recovery flow.
11
+ *
12
+ * Private-mailbox-only response shape: `IsGhosted` is always `0` and its conditional
13
+ * `ServerCount`/`CheapServerCount`/`Servers` fields are never present, since `IsGhosted` and its trailing
14
+ * fields are documented as public-folder-only (`[MS-OXCFOLD]`'s own "RopOpenFolder ROP Response Buffer"
15
+ * page). `HasRules` is always `0` - this library has no `[MS-OXORULE]` rules-engine support.
16
+ *
17
+ * @author Jean-Philippe Steinmetz
18
+ */
19
+ export class RopOpenFolderHandler {
20
+ constructor() {
21
+ this.ropId = ROP_ID_OPEN_FOLDER;
22
+ }
23
+ handle(reader, writer, context) {
24
+ reader.readUInt8(); // LogonId - this pragmatic subset doesn't track multiple concurrent logons per session
25
+ reader.readUInt8(); // InputHandleIndex - the logon handle this open is performed against; only one
26
+ // logon exists per session in this pragmatic subset, so it's consumed to advance the reader correctly
27
+ // but never separately validated.
28
+ const outputHandleIndex = reader.readUInt8();
29
+ reader.readUInt8(); // OpenModeFlags - OpenSoftDeleted not supported in this pragmatic subset
30
+ const folderId = reader.readBigUInt64LE();
31
+ const target = context.session.folderIds[folderId.toString()];
32
+ if (!target) {
33
+ writer.writeUInt8(ROP_ID_OPEN_FOLDER);
34
+ writer.writeUInt8(outputHandleIndex);
35
+ writer.writeUInt32LE(ERROR_NOT_FOUND);
36
+ return;
37
+ }
38
+ context.session.handles[outputHandleIndex] = { type: "folder", entityUid: target };
39
+ writer.writeUInt8(ROP_ID_OPEN_FOLDER);
40
+ writer.writeUInt8(outputHandleIndex);
41
+ writer.writeUInt32LE(0); // ReturnValue - success
42
+ writer.writeUInt8(0); // HasRules - no rules-engine support
43
+ writer.writeUInt8(0); // IsGhosted - always false for a private mailbox
44
+ }
45
+ }
46
+ //# sourceMappingURL=RopOpenFolderHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RopOpenFolderHandler.js","sourceRoot":"","sources":["../../../src/rop/RopOpenFolderHandler.ts"],"names":[],"mappings":"AAOA,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAEhC;;oBAEoB;AACpB,MAAM,eAAe,GAAG,UAAU,CAAC;AAEnC;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,oBAAoB;IAAjC;QACoB,UAAK,GAAG,kBAAkB,CAAC;IA2B/C,CAAC;IAzBU,MAAM,CAAC,MAAoB,EAAE,MAAoB,EAAE,OAAmB;QACzE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,uFAAuF;QAC3G,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,+EAA+E;QACnG,sGAAsG;QACtG,kCAAkC;QAClC,MAAM,iBAAiB,GAAW,MAAM,CAAC,SAAS,EAAE,CAAC;QACrD,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,yEAAyE;QAC7F,MAAM,QAAQ,GAAW,MAAM,CAAC,eAAe,EAAE,CAAC;QAElD,MAAM,MAAM,GAAuB,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;QAClF,IAAI,CAAC,MAAM,EAAE,CAAC;YACV,MAAM,CAAC,UAAU,CAAC,kBAAkB,CAAC,CAAC;YACtC,MAAM,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC;YACrC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC;YACtC,OAAO;QACX,CAAC;QAED,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;QAEnF,MAAM,CAAC,UAAU,CAAC,kBAAkB,CAAC,CAAC;QACtC,MAAM,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC;QACrC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,wBAAwB;QACjD,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,qCAAqC;QAC3D,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,iDAAiD;IAC3E,CAAC;CACJ"}
@@ -0,0 +1,81 @@
1
+ import { writeTypedString } from "../codec/TypedString.js";
2
+ import { resolveCalendarEventInfo } from "./CalendarEventTarget.js";
3
+ import { resolveContactInfo } from "./ContactTarget.js";
4
+ import { resolveMessageInfo } from "./MessageTarget.js";
5
+ import { resolveTaskInfo } from "./TaskTarget.js";
6
+ const ROP_ID_OPEN_MESSAGE = 0x03;
7
+ /** The well-known MAPI HRESULT `MAPI_E_NOT_FOUND`, reused as this handler's only failure `ReturnValue` - an
8
+ * unrecognized `MessageId`, i.e. not something a `RopQueryRows` `PidTagMid` column has handed out this session
9
+ * (see `MessageTarget.assignOrGetMid`'s own doc comment). Same constant `RopOpenFolderHandler` uses for its
10
+ * own analogous `FolderId` lookup miss. */
11
+ const ERROR_NOT_FOUND = 0x8004010f;
12
+ /**
13
+ * `RopOpenMessage` (`[MS-OXCMSG]`/`[MS-OXCROPS]`): opens a message by MID, producing a new `"message"` Server
14
+ * object handle later ROPs (`RopGetPropertiesSpecific`, `RopOpenStream`) reference by `InputHandleIndex`. The
15
+ * request's own `FolderId` (the message's expected parent folder) and `CodePageId` are decoded to advance past
16
+ * them correctly but not otherwise acted on - a MID is resolved directly via `session.messageIds`
17
+ * (`MessageTarget.assignOrGetMid`), the same "the session already knows what this small integer refers to"
18
+ * design `RopOpenFolderHandler` uses for FIDs, so cross-checking the caller-supplied `FolderId` would be pure
19
+ * redundancy. `OpenModeFlags` is likewise decoded but not honored - this pragmatic subset only ever opens a
20
+ * message for reading (no `RopSetProperties`/`RopSaveChangesMessage` write-back support yet).
21
+ *
22
+ * **Pragmatic success response**: `HasNamedProperties` is always `false` - a conservative hint value only, not
23
+ * a claim that named properties are unsupported (`RopGetPropertyIdsFromNames`/`PropertyResolvers.ts` do resolve
24
+ * them for a subsequently-opened Calendar item; a real client calls `RopGetPropertyIdsFromNames` unconditionally
25
+ * for a known Calendar message class regardless of this flag, so leaving it `false` doesn't block that path).
26
+ * The `SubjectPrefix`/`NormalizedSubject` `TypedString`s are "no prefix" + the item's plain subject/title (this
27
+ * data model has no separate prefix/normalized-subject split the way real Exchange does), and the recipient
28
+ * table (`RecipientCount`/`ColumnCount`/`RecipientColumns`/`RowCount`/`RecipientRows`) is always empty - reading
29
+ * a message's `To`/`Cc` list is a documented gap in this pass, not silently wrong data.
30
+ *
31
+ * @author Jean-Philippe Steinmetz
32
+ */
33
+ export class RopOpenMessageHandler {
34
+ constructor() {
35
+ this.ropId = ROP_ID_OPEN_MESSAGE;
36
+ }
37
+ async handle(reader, writer, context) {
38
+ reader.readUInt8(); // LogonId - this pragmatic subset doesn't track multiple concurrent logons per session
39
+ reader.readUInt8(); // InputHandleIndex - the folder/logon handle this open is performed against; not
40
+ // separately validated, matching RopOpenFolderHandler's own treatment of its input handle.
41
+ const outputHandleIndex = reader.readUInt8();
42
+ reader.readUInt16LE(); // CodePageId - this pragmatic subset always encodes strings as UTF-16LE/UTF-8 directly
43
+ reader.readBigUInt64LE(); // FolderId - the message's expected parent folder; not cross-checked, see class doc comment
44
+ reader.readUInt8(); // OpenModeFlags - read-only opens only in this pragmatic subset
45
+ const messageId = reader.readBigUInt64LE();
46
+ const target = context.session.messageIds[messageId.toString()];
47
+ if (!target) {
48
+ writer.writeUInt8(ROP_ID_OPEN_MESSAGE);
49
+ writer.writeUInt8(outputHandleIndex);
50
+ writer.writeUInt32LE(ERROR_NOT_FOUND);
51
+ return;
52
+ }
53
+ // A calendar/contact/task item's "subject" is its title/display name (PidTagSubject, the same tag a
54
+ // Message uses) - see CalendarEventTarget.ts's own doc comment for why no separate MID-registry
55
+ // mechanism is needed for any of these target kinds.
56
+ let subject;
57
+ if (target.startsWith("calendarEvent:")) {
58
+ subject = (await resolveCalendarEventInfo(target, context.calendarEventRepo)).title;
59
+ }
60
+ else if (target.startsWith("contact:")) {
61
+ subject = context.contactRepo ? (await resolveContactInfo(target, context.contactRepo)).displayName : "";
62
+ }
63
+ else if (target.startsWith("task:")) {
64
+ subject = context.taskRepo ? (await resolveTaskInfo(target, context.taskRepo)).title : "";
65
+ }
66
+ else {
67
+ subject = (await resolveMessageInfo(target, context.messageRepo)).subject;
68
+ }
69
+ context.session.handles[outputHandleIndex] = { type: "message", entityUid: target };
70
+ writer.writeUInt8(ROP_ID_OPEN_MESSAGE);
71
+ writer.writeUInt8(outputHandleIndex);
72
+ writer.writeUInt32LE(0); // ReturnValue - success
73
+ writer.writeUInt8(0); // HasNamedProperties - conservative fixed value, see class doc comment
74
+ writeTypedString(writer, undefined); // SubjectPrefix - no prefix/normalized-subject split in this data model
75
+ writeTypedString(writer, subject); // NormalizedSubject
76
+ writer.writeUInt16LE(0); // RecipientCount - reading recipients is a documented gap, see class doc comment
77
+ writer.writeUInt16LE(0); // ColumnCount
78
+ writer.writeUInt8(0); // RowCount
79
+ }
80
+ }
81
+ //# sourceMappingURL=RopOpenMessageHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RopOpenMessageHandler.js","sourceRoot":"","sources":["../../../src/rop/RopOpenMessageHandler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ import { PropertyType, readPropertyTag } from "../codec/PropertyValue.js";
2
+ import { PID_TAG_BODY, resolveMessageBodyBytes } from "./MessageBodyStream.js";
3
+ const ROP_ID_OPEN_STREAM = 0x2b;
4
+ /** `[MS-OXCPRPT]` §2.2.14.1 `OpenModeFlags` values - `ReadOnly` opens the existing value for reading;
5
+ * `ReadWrite`/`Create` both open for writing (`Create` additionally discards any existing value first, "the
6
+ * mode required for a property that has not been set" per the spec - always true for a fresh
7
+ * `RopCreateMessage` draft's body, which is the only write-mode case this pragmatic subset ever produces). */
8
+ const OPEN_MODE_READ_ONLY = 0x00;
9
+ /** The well-known MAPI HRESULT `MAPI_E_NOT_FOUND`, reused for "the referenced handle isn't a message, or the
10
+ * requested property isn't one this pragmatic subset can stream" - the same constant `RopOpenFolderHandler`
11
+ * uses for its own analogous lookup-miss case. */
12
+ const ERROR_NOT_FOUND = 0x8004010f;
13
+ /**
14
+ * `RopOpenStream` (`[MS-OXCPRPT]`/`[MS-OXCROPS]`): opens a property for streaming access, producing a new
15
+ * `"stream"` Server object handle `RopReadStream`/`RopWriteStream` reads/writes by `InputHandleIndex`.
16
+ * **Pragmatic subset**: only `PidTagBody` (`PtypString`, the plain-text message body) on a `"message"` handle
17
+ * is supported - a real client also streams `PidTagHtml`/`PidTagRtfCompressed` (rich body formats) and
18
+ * folder/attachment binary properties, none of which this pass implements; requesting any other `PropertyTag`,
19
+ * or opening a stream against a non-message handle, fails with `MAPI_E_NOT_FOUND` rather than succeeding with
20
+ * wrong data.
21
+ *
22
+ * `OpenModeFlags` (`[MS-OXCPRPT]` §2.2.14.1) selects which of two independent code paths runs:
23
+ * - `ReadOnly` (`0x00`): the step-7 behavior - resolves the real, already-saved message's body via
24
+ * `MessageBodyStream.resolveMessageBodyBytes()` (`BlobStore` + `mailparser`) and reports its length as
25
+ * `StreamSize`, for `RopReadStream` to read back.
26
+ * - `ReadWrite`/`Create` (`0x01`/`0x02`): a **write-mode** stream, the step-8 addition compose/send needs to
27
+ * accept a message body via `RopWriteStream` - `StreamSize` is always `0` (matching `Create`'s "discards the
28
+ * existing value" semantics; this pragmatic subset never opens `ReadWrite` against a body that already has
29
+ * content) and the new handle's `writeTargetHandleIndex` remembers which message handle to write back into,
30
+ * the link `RopWriteStream`/`RopSaveChangesMessageHandler`/`RopSubmitMessageHandler` use to find it again.
31
+ *
32
+ * @author Jean-Philippe Steinmetz
33
+ */
34
+ export class RopOpenStreamHandler {
35
+ constructor() {
36
+ this.ropId = ROP_ID_OPEN_STREAM;
37
+ }
38
+ async handle(reader, writer, context) {
39
+ reader.readUInt8(); // LogonId - this pragmatic subset doesn't track multiple concurrent logons per session
40
+ const inputHandleIndex = reader.readUInt8();
41
+ const outputHandleIndex = reader.readUInt8();
42
+ const propertyTag = readPropertyTag(reader);
43
+ const openModeFlags = reader.readUInt8();
44
+ const handle = context.session.handles[inputHandleIndex];
45
+ const isSupportedProperty = propertyTag.propertyId === PID_TAG_BODY && propertyTag.propertyType === PropertyType.PtypString;
46
+ if (!handle || handle.type !== "message" || !isSupportedProperty) {
47
+ writer.writeUInt8(ROP_ID_OPEN_STREAM);
48
+ writer.writeUInt8(outputHandleIndex);
49
+ writer.writeUInt32LE(ERROR_NOT_FOUND);
50
+ return;
51
+ }
52
+ let streamSize;
53
+ if (openModeFlags === OPEN_MODE_READ_ONLY) {
54
+ const bytes = await resolveMessageBodyBytes(handle.entityUid, context.messageRepo, context.blobStore);
55
+ streamSize = bytes.length;
56
+ context.session.handles[outputHandleIndex] = {
57
+ type: "stream",
58
+ entityUid: handle.entityUid,
59
+ propertyId: propertyTag.propertyId,
60
+ propertyType: propertyTag.propertyType,
61
+ streamPosition: 0,
62
+ };
63
+ }
64
+ else {
65
+ streamSize = 0;
66
+ context.session.handles[outputHandleIndex] = {
67
+ type: "stream",
68
+ entityUid: handle.entityUid,
69
+ propertyId: propertyTag.propertyId,
70
+ propertyType: propertyTag.propertyType,
71
+ writeTargetHandleIndex: inputHandleIndex,
72
+ writeBufferBase64: "",
73
+ };
74
+ }
75
+ writer.writeUInt8(ROP_ID_OPEN_STREAM);
76
+ writer.writeUInt8(outputHandleIndex);
77
+ writer.writeUInt32LE(0); // ReturnValue - success
78
+ writer.writeUInt32LE(streamSize); // StreamSize
79
+ }
80
+ }
81
+ //# sourceMappingURL=RopOpenStreamHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RopOpenStreamHandler.js","sourceRoot":"","sources":["../../../src/rop/RopOpenStreamHandler.ts"],"names":[],"mappings":"AAKA,OAAO,EAAe,YAAY,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACvF,OAAO,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAG/E,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAEhC;;;8GAG8G;AAC9G,MAAM,mBAAmB,GAAG,IAAI,CAAC;AAEjC;;kDAEkD;AAClD,MAAM,eAAe,GAAG,UAAU,CAAC;AAEnC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,OAAO,oBAAoB;IAAjC;QACoB,UAAK,GAAG,kBAAkB,CAAC;IA8C/C,CAAC;IA5CU,KAAK,CAAC,MAAM,CAAC,MAAoB,EAAE,MAAoB,EAAE,OAAmB;QAC/E,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,uFAAuF;QAC3G,MAAM,gBAAgB,GAAW,MAAM,CAAC,SAAS,EAAE,CAAC;QACpD,MAAM,iBAAiB,GAAW,MAAM,CAAC,SAAS,EAAE,CAAC;QACrD,MAAM,WAAW,GAAgB,eAAe,CAAC,MAAM,CAAC,CAAC;QACzD,MAAM,aAAa,GAAW,MAAM,CAAC,SAAS,EAAE,CAAC;QAEjD,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;QACzD,MAAM,mBAAmB,GAAG,WAAW,CAAC,UAAU,KAAK,YAAY,IAAI,WAAW,CAAC,YAAY,KAAK,YAAY,CAAC,UAAU,CAAC;QAC5H,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAC/D,MAAM,CAAC,UAAU,CAAC,kBAAkB,CAAC,CAAC;YACtC,MAAM,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC;YACrC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC;YACtC,OAAO;QACX,CAAC;QAED,IAAI,UAAkB,CAAC;QACvB,IAAI,aAAa,KAAK,mBAAmB,EAAE,CAAC;YACxC,MAAM,KAAK,GAAG,MAAM,uBAAuB,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;YACtG,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC;YAC1B,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,GAAG;gBACzC,IAAI,EAAE,QAAQ;gBACd,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,UAAU,EAAE,WAAW,CAAC,UAAU;gBAClC,YAAY,EAAE,WAAW,CAAC,YAAY;gBACtC,cAAc,EAAE,CAAC;aACpB,CAAC;QACN,CAAC;aAAM,CAAC;YACJ,UAAU,GAAG,CAAC,CAAC;YACf,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,GAAG;gBACzC,IAAI,EAAE,QAAQ;gBACd,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,UAAU,EAAE,WAAW,CAAC,UAAU;gBAClC,YAAY,EAAE,WAAW,CAAC,YAAY;gBACtC,sBAAsB,EAAE,gBAAgB;gBACxC,iBAAiB,EAAE,EAAE;aACxB,CAAC;QACN,CAAC;QAED,MAAM,CAAC,UAAU,CAAC,kBAAkB,CAAC,CAAC;QACtC,MAAM,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC;QACrC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,wBAAwB;QACjD,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,CAAC,aAAa;IACnD,CAAC;CACJ"}
@@ -0,0 +1,91 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { BufferWriter } from "../codec/BufferCursor.js";
6
+ import { writePropertyValue } from "../codec/PropertyValue.js";
7
+ import { resolvePropertyValues } from "./PropertyResolvers.js";
8
+ const ROP_ID_QUERY_ROWS = 0x15;
9
+ /** The well-known MAPI HRESULT `MAPI_E_INVALID_OBJECT`, reused for "the referenced handle isn't a table (or
10
+ * doesn't exist)" - the same constant `RopGetHierarchyTableHandler` uses for its own analogous check. */
11
+ const ERROR_INVALID_OBJECT = 0x80070005;
12
+ /** `BOOKMARK_END` (`[MS-OXCTABL]`'s `Origin` field) - this pragmatic subset has no separate seek/bookmark ROP
13
+ * support, so every response reports the cursor as having landed at the table's current end. */
14
+ const ORIGIN_BOOKMARK_END = 0x02;
15
+ /**
16
+ * `RopQueryRows` (`[MS-OXCTABL]`/`[MS-OXCROPS]`): fetches up to `RowCount` rows from an already-configured
17
+ * table (`RopGetHierarchyTable` + `RopSetColumns`), advancing the table's cursor. Confirmed field-by-field
18
+ * against `[MS-OXCTABL]`'s own real captured request/response byte example - including the response's
19
+ * `RowData` encoding (`PropertyRow`: a leading `Flags` byte, `0x00` for a `StandardPropertyRow` where every
20
+ * column is a plain `PropertyValue`, `0x01` for a `FlaggedPropertyRow` where each column gets its own
21
+ * `FlaggedPropertyValue` flag+optional-value). This pragmatic subset always emits `StandardPropertyRow`
22
+ * (`Flags = 0x00`) - every configured column always resolves to *some* value here (falling back to a
23
+ * type-appropriate zero/empty default for an unsupported property, never an error), so `FlaggedPropertyRow`'s
24
+ * per-column error signaling is never needed.
25
+ *
26
+ * Backward reads (`ForwardRead = FALSE`) and the `NoAdvance` flag are decoded (to advance the reader
27
+ * correctly) but not honored - this pragmatic subset's tables are simple forward-only cursors. Works generically
28
+ * against either a `RopGetHierarchyTable` (folder rows, `"folder:"`/`"virtual:"` targets) or a
29
+ * `RopGetContentsTable` (message rows, `"message:"` targets) handle - dispatching to `FolderTarget`'s or
30
+ * `MessageTarget`'s resolver and property-value mapping by row-target prefix, since a single table handle only
31
+ * ever holds one kind of row.
32
+ *
33
+ * **Known, deliberate performance limitation**: `resolvePropertyValues` still does one `repo.findOne()` per
34
+ * row (message/calendarEvent/contact/task) rather than a single batched fetch for the whole page - a genuine
35
+ * N+1 for a large `RowCount`. A real fix needs a batched "fetch all these uids" query, which this codebase's
36
+ * `RepoUtils<T>` abstraction doesn't expose a backend-agnostic way to express (Mongo's `$in` and TypeORM's
37
+ * `In()` aren't interchangeable at this call site, which has no idea which backend it's running against) - see
38
+ * `ResolutionCache` for the narrower, safe win this pass *did* make (the redundant *identical* whole-mailbox-
39
+ * folder-list and calendar-mailbox fetches that were happening once per row regardless of which distinct
40
+ * entity each row was, now happen at most once per call).
41
+ *
42
+ * @author Jean-Philippe Steinmetz
43
+ */
44
+ export class RopQueryRowsHandler {
45
+ constructor() {
46
+ this.ropId = ROP_ID_QUERY_ROWS;
47
+ }
48
+ async handle(reader, writer, context) {
49
+ reader.readUInt8(); // LogonId - this pragmatic subset doesn't track multiple concurrent logons per session
50
+ const inputHandleIndex = reader.readUInt8();
51
+ reader.readUInt8(); // QueryRowsFlags - NoAdvance/EnablePackedBuffers not supported; always advances
52
+ reader.readUInt8(); // ForwardRead - backward reads not supported; always reads forward
53
+ const requestedCount = reader.readUInt16LE();
54
+ const table = context.session.handles[inputHandleIndex];
55
+ if (!table || table.type !== "table") {
56
+ writer.writeUInt8(ROP_ID_QUERY_ROWS);
57
+ writer.writeUInt8(inputHandleIndex);
58
+ writer.writeUInt32LE(ERROR_INVALID_OBJECT);
59
+ return;
60
+ }
61
+ const rows = table.rows ?? [];
62
+ const cursor = table.cursor ?? 0;
63
+ const slice = rows.slice(cursor, cursor + requestedCount);
64
+ table.cursor = cursor + slice.length;
65
+ // Shared across every row this call resolves - see ResolutionCache's own doc comment for why: without
66
+ // it, a hierarchy table's `hasChildren` column (or a calendar table's organizer/attendee resolution)
67
+ // would re-fetch the same whole-mailbox folder list/mailbox record once per row instead of once per
68
+ // call.
69
+ const cache = {};
70
+ const rowBuffers = [];
71
+ for (const target of slice) {
72
+ rowBuffers.push(await this.buildRow(context, target, table.columns ?? [], cache));
73
+ }
74
+ writer.writeUInt8(ROP_ID_QUERY_ROWS);
75
+ writer.writeUInt8(inputHandleIndex);
76
+ writer.writeUInt32LE(0); // ReturnValue - success
77
+ writer.writeUInt8(ORIGIN_BOOKMARK_END);
78
+ writer.writeUInt16LE(slice.length);
79
+ for (const rowBuffer of rowBuffers) {
80
+ writer.writeBytes(rowBuffer);
81
+ }
82
+ }
83
+ async buildRow(context, target, columns, cache) {
84
+ const writer = new BufferWriter();
85
+ writer.writeUInt8(0x00); // Flags - StandardPropertyRow, see class doc comment
86
+ const values = await resolvePropertyValues(target, columns, context, cache);
87
+ columns.forEach((column, index) => writePropertyValue(writer, column.propertyType, values[index]));
88
+ return writer.toBuffer();
89
+ }
90
+ }
91
+ //# sourceMappingURL=RopQueryRowsHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RopQueryRowsHandler.js","sourceRoot":"","sources":["../../../src/rop/RopQueryRowsHandler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import { resolveMessageBodyBytes } from "./MessageBodyStream.js";
2
+ const ROP_ID_READ_STREAM = 0x2c;
3
+ /** The well-known MAPI HRESULT `MAPI_E_INVALID_OBJECT`, reused for "the referenced handle isn't a stream (or
4
+ * doesn't exist)" - the same constant `RopGetHierarchyTableHandler` uses for its own analogous check. */
5
+ const ERROR_INVALID_OBJECT = 0x80070005;
6
+ /** `0xBABE`: `ByteCount`'s sentinel value signaling "the real limit is the following 4-byte `MaximumByteCount`
7
+ * field instead" (`[MS-OXCPRPT]`'s own `RopReadStream` request-buffer page). */
8
+ const BYTE_COUNT_USE_MAXIMUM = 0xbabe;
9
+ /** The response's own `DataSize` field is a 16-bit count (`[MS-OXCPRPT]`'s own `RopReadStream` response-buffer
10
+ * page), so a single call can never actually return more than this many bytes - regardless of what `ByteCount`/
11
+ * `MaximumByteCount` (a full 32-bit value in the latter case) the client asked for. Returning fewer bytes than
12
+ * requested is spec-legal (the client just calls `RopReadStream` again for the rest, exactly how paging a large
13
+ * stream is meant to work) - `writeUInt16LE` throwing `RangeError` for an out-of-range value is not. */
14
+ const MAX_DATA_SIZE = 0xffff;
15
+ /**
16
+ * `RopReadStream` (`[MS-OXCPRPT]`/`[MS-OXCROPS]`): reads up to `ByteCount` (or `MaximumByteCount`, if
17
+ * `ByteCount` is the `0xBABE` sentinel) bytes from an already-opened stream (`RopOpenStream`), advancing the
18
+ * stream's read position - clamped to `MAX_DATA_SIZE` (`0xFFFF`) regardless of what the client asked for, since
19
+ * the response's own `DataSize` field can't carry more than that; a client wanting more of a large body simply
20
+ * calls `RopReadStream` again from the advanced `streamPosition`, the same paging behavior real Exchange uses.
21
+ * Unlike most other ROPs in this pragmatic subset, `[MS-OXCROPS]` documents only one combined response-buffer
22
+ * shape for this ROP (no separate Success/Failure pages) - `DataSize`/`Data` are always present, `DataSize = 0`
23
+ * (empty `Data`) standing in for the failure case rather than the fields being omitted entirely.
24
+ *
25
+ * @author Jean-Philippe Steinmetz
26
+ */
27
+ export class RopReadStreamHandler {
28
+ constructor() {
29
+ this.ropId = ROP_ID_READ_STREAM;
30
+ }
31
+ async handle(reader, writer, context) {
32
+ reader.readUInt8(); // LogonId - this pragmatic subset doesn't track multiple concurrent logons per session
33
+ const inputHandleIndex = reader.readUInt8();
34
+ const byteCount = reader.readUInt16LE();
35
+ const requestedCount = byteCount === BYTE_COUNT_USE_MAXIMUM ? reader.readUInt32LE() : byteCount;
36
+ const handle = context.session.handles[inputHandleIndex];
37
+ if (!handle || handle.type !== "stream") {
38
+ writer.writeUInt8(ROP_ID_READ_STREAM);
39
+ writer.writeUInt8(inputHandleIndex);
40
+ writer.writeUInt32LE(ERROR_INVALID_OBJECT);
41
+ writer.writeUInt16LE(0); // DataSize - see class doc comment on why this is still written on failure
42
+ return;
43
+ }
44
+ const bytes = await resolveMessageBodyBytes(handle.entityUid, context.messageRepo, context.blobStore);
45
+ const position = handle.streamPosition ?? 0;
46
+ const slice = bytes.subarray(position, position + Math.min(requestedCount, MAX_DATA_SIZE));
47
+ handle.streamPosition = position + slice.length;
48
+ writer.writeUInt8(ROP_ID_READ_STREAM);
49
+ writer.writeUInt8(inputHandleIndex);
50
+ writer.writeUInt32LE(0); // ReturnValue - success
51
+ writer.writeUInt16LE(slice.length); // DataSize
52
+ writer.writeBytes(slice);
53
+ }
54
+ }
55
+ //# sourceMappingURL=RopReadStreamHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RopReadStreamHandler.js","sourceRoot":"","sources":["../../../src/rop/RopReadStreamHandler.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAGjE,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAEhC;yGACyG;AACzG,MAAM,oBAAoB,GAAG,UAAU,CAAC;AAExC;gFACgF;AAChF,MAAM,sBAAsB,GAAG,MAAM,CAAC;AAEtC;;;;wGAIwG;AACxG,MAAM,aAAa,GAAG,MAAM,CAAC;AAE7B;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,oBAAoB;IAAjC;QACoB,UAAK,GAAG,kBAAkB,CAAC;IA4B/C,CAAC;IA1BU,KAAK,CAAC,MAAM,CAAC,MAAoB,EAAE,MAAoB,EAAE,OAAmB;QAC/E,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,uFAAuF;QAC3G,MAAM,gBAAgB,GAAW,MAAM,CAAC,SAAS,EAAE,CAAC;QACpD,MAAM,SAAS,GAAW,MAAM,CAAC,YAAY,EAAE,CAAC;QAChD,MAAM,cAAc,GAAW,SAAS,KAAK,sBAAsB,CAAC,CAAC,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;QAExG,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;QACzD,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACtC,MAAM,CAAC,UAAU,CAAC,kBAAkB,CAAC,CAAC;YACtC,MAAM,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;YACpC,MAAM,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAC;YAC3C,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,2EAA2E;YACpG,OAAO;QACX,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,uBAAuB,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QACtG,MAAM,QAAQ,GAAG,MAAM,CAAC,cAAc,IAAI,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,aAAa,CAAC,CAAC,CAAC;QAC3F,MAAM,CAAC,cAAc,GAAG,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC;QAEhD,MAAM,CAAC,UAAU,CAAC,kBAAkB,CAAC,CAAC;QACtC,MAAM,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;QACpC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,wBAAwB;QACjD,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW;QAC/C,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IAC7B,CAAC;CACJ"}
@@ -0,0 +1,20 @@
1
+ const ROP_ID_RELEASE = 0x01;
2
+ /**
3
+ * `RopRelease` (`[MS-OXCROPS]`): releases a Server object handle. Confirmed against the spec's own real
4
+ * captured example (`08 00 01 00 00 01 00 01 6F 00 00 00 6E 00 00 00`) that the request is exactly 3 bytes
5
+ * (`RopId`/`LogonId`/`InputHandleIndex`) and, uniquely among ROPs, **produces no response entry at all** - the
6
+ * server MUST NOT write anything to the output buffer for it, so `handle()` never touches `writer`.
7
+ *
8
+ * @author Jean-Philippe Steinmetz
9
+ */
10
+ export class RopReleaseHandler {
11
+ constructor() {
12
+ this.ropId = ROP_ID_RELEASE;
13
+ }
14
+ handle(reader, _writer, context) {
15
+ reader.readUInt8(); // LogonId - this pragmatic subset doesn't track per-logon-id state separately
16
+ const inputHandleIndex = reader.readUInt8();
17
+ delete context.session.handles[inputHandleIndex];
18
+ }
19
+ }
20
+ //# sourceMappingURL=RopReleaseHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RopReleaseHandler.js","sourceRoot":"","sources":["../../../src/rop/RopReleaseHandler.ts"],"names":[],"mappings":"AAOA,MAAM,cAAc,GAAG,IAAI,CAAC;AAE5B;;;;;;;GAOG;AACH,MAAM,OAAO,iBAAiB;IAA9B;QACoB,UAAK,GAAG,cAAc,CAAC;IAO3C,CAAC;IALU,MAAM,CAAC,MAAoB,EAAE,OAAqB,EAAE,OAAmB;QAC1E,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,8EAA8E;QAClG,MAAM,gBAAgB,GAAW,MAAM,CAAC,SAAS,EAAE,CAAC;QACpD,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACrD,CAAC;CACJ"}