@rapidmx/mapi-plugin 1.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/LICENSE +382 -0
  2. package/README.md +60 -0
  3. package/dist/lib/BaseMapiEmsmdbRoute.js +489 -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 +475 -0
  8. package/dist/lib/MapiSessionManager.js.map +1 -0
  9. package/dist/lib/RegexPatternUtils.js +30 -0
  10. package/dist/lib/RegexPatternUtils.js.map +1 -0
  11. package/dist/lib/RopDispatcher.js +145 -0
  12. package/dist/lib/RopDispatcher.js.map +1 -0
  13. package/dist/lib/codec/AppointmentRecurrence.js +315 -0
  14. package/dist/lib/codec/AppointmentRecurrence.js.map +1 -0
  15. package/dist/lib/codec/BufferCursor.js +242 -0
  16. package/dist/lib/codec/BufferCursor.js.map +1 -0
  17. package/dist/lib/codec/GlobalObjectId.js +98 -0
  18. package/dist/lib/codec/GlobalObjectId.js.map +1 -0
  19. package/dist/lib/codec/MapiGuid.js +41 -0
  20. package/dist/lib/codec/MapiGuid.js.map +1 -0
  21. package/dist/lib/codec/MapiTimeZone.js +126 -0
  22. package/dist/lib/codec/MapiTimeZone.js.map +1 -0
  23. package/dist/lib/codec/PropertyValue.js +240 -0
  24. package/dist/lib/codec/PropertyValue.js.map +1 -0
  25. package/dist/lib/codec/RopBuffer.js +38 -0
  26. package/dist/lib/codec/RopBuffer.js.map +1 -0
  27. package/dist/lib/codec/TypedString.js +54 -0
  28. package/dist/lib/codec/TypedString.js.map +1 -0
  29. package/dist/lib/index.js +99 -0
  30. package/dist/lib/index.js.map +1 -0
  31. package/dist/lib/mongo/MapiEmsmdbRouteMongo.js +85 -0
  32. package/dist/lib/mongo/MapiEmsmdbRouteMongo.js.map +1 -0
  33. package/dist/lib/mongo/MapiNspiRouteMongo.js +33 -0
  34. package/dist/lib/mongo/MapiNspiRouteMongo.js.map +1 -0
  35. package/dist/lib/mongo/index.js +7 -0
  36. package/dist/lib/mongo/index.js.map +1 -0
  37. package/dist/lib/mongo.js +6 -0
  38. package/dist/lib/mongo.js.map +1 -0
  39. package/dist/lib/nspi/NspiBindHandler.js +54 -0
  40. package/dist/lib/nspi/NspiBindHandler.js.map +1 -0
  41. package/dist/lib/nspi/NspiCodec.js +106 -0
  42. package/dist/lib/nspi/NspiCodec.js.map +1 -0
  43. package/dist/lib/nspi/NspiGetMatchesHandler.js +142 -0
  44. package/dist/lib/nspi/NspiGetMatchesHandler.js.map +1 -0
  45. package/dist/lib/rop/AddressList.js +114 -0
  46. package/dist/lib/rop/AddressList.js.map +1 -0
  47. package/dist/lib/rop/CalendarEventTarget.js +30 -0
  48. package/dist/lib/rop/CalendarEventTarget.js.map +1 -0
  49. package/dist/lib/rop/CalendarNamedProperties.js +57 -0
  50. package/dist/lib/rop/CalendarNamedProperties.js.map +1 -0
  51. package/dist/lib/rop/ContactTarget.js +19 -0
  52. package/dist/lib/rop/ContactTarget.js.map +1 -0
  53. package/dist/lib/rop/ContentsTable.js +60 -0
  54. package/dist/lib/rop/ContentsTable.js.map +1 -0
  55. package/dist/lib/rop/ExecuteBudget.js +82 -0
  56. package/dist/lib/rop/ExecuteBudget.js.map +1 -0
  57. package/dist/lib/rop/FastTransferStream.js +195 -0
  58. package/dist/lib/rop/FastTransferStream.js.map +1 -0
  59. package/dist/lib/rop/FolderTarget.js +118 -0
  60. package/dist/lib/rop/FolderTarget.js.map +1 -0
  61. package/dist/lib/rop/HandleDataCache.js +412 -0
  62. package/dist/lib/rop/HandleDataCache.js.map +1 -0
  63. package/dist/lib/rop/MeetingMessageClassHandler.js +241 -0
  64. package/dist/lib/rop/MeetingMessageClassHandler.js.map +1 -0
  65. package/dist/lib/rop/MessageBodyStream.js +68 -0
  66. package/dist/lib/rop/MessageBodyStream.js.map +1 -0
  67. package/dist/lib/rop/MessageTarget.js +57 -0
  68. package/dist/lib/rop/MessageTarget.js.map +1 -0
  69. package/dist/lib/rop/NamedPropertyRegistry.js +69 -0
  70. package/dist/lib/rop/NamedPropertyRegistry.js.map +1 -0
  71. package/dist/lib/rop/PropertyResolvers.js +353 -0
  72. package/dist/lib/rop/PropertyResolvers.js.map +1 -0
  73. package/dist/lib/rop/RepoPaging.js +54 -0
  74. package/dist/lib/rop/RepoPaging.js.map +1 -0
  75. package/dist/lib/rop/RestapiRules.js +52 -0
  76. package/dist/lib/rop/RestapiRules.js.map +1 -0
  77. package/dist/lib/rop/RopCreateMessageHandler.js +55 -0
  78. package/dist/lib/rop/RopCreateMessageHandler.js.map +1 -0
  79. package/dist/lib/rop/RopDeleteFolderHandler.js +174 -0
  80. package/dist/lib/rop/RopDeleteFolderHandler.js.map +1 -0
  81. package/dist/lib/rop/RopDeleteMessagesHandler.js +110 -0
  82. package/dist/lib/rop/RopDeleteMessagesHandler.js.map +1 -0
  83. package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js +61 -0
  84. package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js.map +1 -0
  85. package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js +60 -0
  86. package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js.map +1 -0
  87. package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js +76 -0
  88. package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js.map +1 -0
  89. package/dist/lib/rop/RopGetContentsTableHandler.js +55 -0
  90. package/dist/lib/rop/RopGetContentsTableHandler.js.map +1 -0
  91. package/dist/lib/rop/RopGetHierarchyTableHandler.js +42 -0
  92. package/dist/lib/rop/RopGetHierarchyTableHandler.js.map +1 -0
  93. package/dist/lib/rop/RopGetPropertiesSpecificHandler.js +56 -0
  94. package/dist/lib/rop/RopGetPropertiesSpecificHandler.js.map +1 -0
  95. package/dist/lib/rop/RopGetPropertyIdsFromNamesHandler.js +92 -0
  96. package/dist/lib/rop/RopGetPropertyIdsFromNamesHandler.js.map +1 -0
  97. package/dist/lib/rop/RopHandler.js +6 -0
  98. package/dist/lib/rop/RopHandler.js.map +1 -0
  99. package/dist/lib/rop/RopLogonHandler.js +135 -0
  100. package/dist/lib/rop/RopLogonHandler.js.map +1 -0
  101. package/dist/lib/rop/RopOpenFolderHandler.js +48 -0
  102. package/dist/lib/rop/RopOpenFolderHandler.js.map +1 -0
  103. package/dist/lib/rop/RopOpenMessageHandler.js +83 -0
  104. package/dist/lib/rop/RopOpenMessageHandler.js.map +1 -0
  105. package/dist/lib/rop/RopOpenStreamHandler.js +84 -0
  106. package/dist/lib/rop/RopOpenStreamHandler.js.map +1 -0
  107. package/dist/lib/rop/RopQueryRowsHandler.js +120 -0
  108. package/dist/lib/rop/RopQueryRowsHandler.js.map +1 -0
  109. package/dist/lib/rop/RopReadStreamHandler.js +73 -0
  110. package/dist/lib/rop/RopReadStreamHandler.js.map +1 -0
  111. package/dist/lib/rop/RopReleaseHandler.js +23 -0
  112. package/dist/lib/rop/RopReleaseHandler.js.map +1 -0
  113. package/dist/lib/rop/RopSaveChangesMessageHandler.js +269 -0
  114. package/dist/lib/rop/RopSaveChangesMessageHandler.js.map +1 -0
  115. package/dist/lib/rop/RopSetColumnsHandler.js +50 -0
  116. package/dist/lib/rop/RopSetColumnsHandler.js.map +1 -0
  117. package/dist/lib/rop/RopSetPropertiesHandler.js +173 -0
  118. package/dist/lib/rop/RopSetPropertiesHandler.js.map +1 -0
  119. package/dist/lib/rop/RopSubmitMessageHandler.js +342 -0
  120. package/dist/lib/rop/RopSubmitMessageHandler.js.map +1 -0
  121. package/dist/lib/rop/RopWriteStreamHandler.js +72 -0
  122. package/dist/lib/rop/RopWriteStreamHandler.js.map +1 -0
  123. package/dist/lib/rop/TaskNamedProperties.js +29 -0
  124. package/dist/lib/rop/TaskNamedProperties.js.map +1 -0
  125. package/dist/lib/rop/TaskTarget.js +15 -0
  126. package/dist/lib/rop/TaskTarget.js.map +1 -0
  127. package/dist/lib/rop/TransportSend.js +16 -0
  128. package/dist/lib/rop/TransportSend.js.map +1 -0
  129. package/dist/lib/sql/MapiEmsmdbRouteSQL.js +85 -0
  130. package/dist/lib/sql/MapiEmsmdbRouteSQL.js.map +1 -0
  131. package/dist/lib/sql/MapiNspiRouteSQL.js +33 -0
  132. package/dist/lib/sql/MapiNspiRouteSQL.js.map +1 -0
  133. package/dist/lib/sql/index.js +7 -0
  134. package/dist/lib/sql/index.js.map +1 -0
  135. package/dist/lib/sql.js +6 -0
  136. package/dist/lib/sql.js.map +1 -0
  137. package/dist/types/BaseMapiEmsmdbRoute.d.ts +139 -0
  138. package/dist/types/BaseMapiNspiRoute.d.ts +38 -0
  139. package/dist/types/MapiSessionManager.d.ts +285 -0
  140. package/dist/types/RegexPatternUtils.d.ts +14 -0
  141. package/dist/types/RopDispatcher.d.ts +53 -0
  142. package/dist/types/codec/AppointmentRecurrence.d.ts +14 -0
  143. package/dist/types/codec/BufferCursor.d.ts +92 -0
  144. package/dist/types/codec/GlobalObjectId.d.ts +15 -0
  145. package/dist/types/codec/MapiGuid.d.ts +6 -0
  146. package/dist/types/codec/MapiTimeZone.d.ts +7 -0
  147. package/dist/types/codec/PropertyValue.d.ts +87 -0
  148. package/dist/types/codec/RopBuffer.d.ts +32 -0
  149. package/dist/types/codec/TypedString.d.ts +17 -0
  150. package/dist/types/index.d.ts +94 -0
  151. package/dist/types/mongo/MapiEmsmdbRouteMongo.d.ts +20 -0
  152. package/dist/types/mongo/MapiNspiRouteMongo.d.ts +13 -0
  153. package/dist/types/mongo/index.d.ts +2 -0
  154. package/dist/types/mongo.d.ts +1 -0
  155. package/dist/types/nspi/NspiBindHandler.d.ts +22 -0
  156. package/dist/types/nspi/NspiCodec.d.ts +59 -0
  157. package/dist/types/nspi/NspiGetMatchesHandler.d.ts +26 -0
  158. package/dist/types/rop/AddressList.d.ts +48 -0
  159. package/dist/types/rop/CalendarEventTarget.d.ts +33 -0
  160. package/dist/types/rop/CalendarNamedProperties.d.ts +37 -0
  161. package/dist/types/rop/ContactTarget.d.ts +29 -0
  162. package/dist/types/rop/ContentsTable.d.ts +16 -0
  163. package/dist/types/rop/ExecuteBudget.d.ts +48 -0
  164. package/dist/types/rop/FastTransferStream.d.ts +57 -0
  165. package/dist/types/rop/FolderTarget.d.ts +64 -0
  166. package/dist/types/rop/HandleDataCache.d.ts +145 -0
  167. package/dist/types/rop/MeetingMessageClassHandler.d.ts +30 -0
  168. package/dist/types/rop/MessageBodyStream.d.ts +35 -0
  169. package/dist/types/rop/MessageTarget.d.ts +45 -0
  170. package/dist/types/rop/NamedPropertyRegistry.d.ts +50 -0
  171. package/dist/types/rop/PropertyResolvers.d.ts +86 -0
  172. package/dist/types/rop/RepoPaging.d.ts +30 -0
  173. package/dist/types/rop/RestapiRules.d.ts +32 -0
  174. package/dist/types/rop/RopCreateMessageHandler.d.ts +24 -0
  175. package/dist/types/rop/RopDeleteFolderHandler.d.ts +53 -0
  176. package/dist/types/rop/RopDeleteMessagesHandler.d.ts +43 -0
  177. package/dist/types/rop/RopFastTransferSourceCopyPropertiesHandler.d.ts +25 -0
  178. package/dist/types/rop/RopFastTransferSourceCopyToHandler.d.ts +25 -0
  179. package/dist/types/rop/RopFastTransferSourceGetBufferHandler.d.ts +22 -0
  180. package/dist/types/rop/RopGetContentsTableHandler.d.ts +25 -0
  181. package/dist/types/rop/RopGetHierarchyTableHandler.d.ts +18 -0
  182. package/dist/types/rop/RopGetPropertiesSpecificHandler.d.ts +26 -0
  183. package/dist/types/rop/RopGetPropertyIdsFromNamesHandler.d.ts +23 -0
  184. package/dist/types/rop/RopHandler.d.ts +106 -0
  185. package/dist/types/rop/RopLogonHandler.d.ts +31 -0
  186. package/dist/types/rop/RopOpenFolderHandler.d.ts +20 -0
  187. package/dist/types/rop/RopOpenMessageHandler.d.ts +28 -0
  188. package/dist/types/rop/RopOpenStreamHandler.d.ts +28 -0
  189. package/dist/types/rop/RopQueryRowsHandler.d.ts +43 -0
  190. package/dist/types/rop/RopReadStreamHandler.d.ts +20 -0
  191. package/dist/types/rop/RopReleaseHandler.d.ts +15 -0
  192. package/dist/types/rop/RopSaveChangesMessageHandler.d.ts +59 -0
  193. package/dist/types/rop/RopSetColumnsHandler.d.ts +21 -0
  194. package/dist/types/rop/RopSetPropertiesHandler.d.ts +38 -0
  195. package/dist/types/rop/RopSubmitMessageHandler.d.ts +80 -0
  196. package/dist/types/rop/RopWriteStreamHandler.d.ts +31 -0
  197. package/dist/types/rop/TaskNamedProperties.d.ts +24 -0
  198. package/dist/types/rop/TaskTarget.d.ts +24 -0
  199. package/dist/types/rop/TransportSend.d.ts +10 -0
  200. package/dist/types/sql/MapiEmsmdbRouteSQL.d.ts +20 -0
  201. package/dist/types/sql/MapiNspiRouteSQL.d.ts +13 -0
  202. package/dist/types/sql/index.d.ts +2 -0
  203. package/dist/types/sql.d.ts +1 -0
  204. package/package.json +104 -0
@@ -0,0 +1,120 @@
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 { resolveContentsWindow } from "./ContentsTable.js";
7
+ import { resolvePropertyValues, writePropertyValueSafely } 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
+ /** The most rows one `RopQueryRows` returns. Fewer rows than requested is normal table paging: the client asks
16
+ * again from the new cursor position. */
17
+ export const MAX_ROWS_PER_QUERY = 500;
18
+ /** `ecBufferTooSmall`: not even one row fits in the room left in the ROP output buffer. */
19
+ const ERROR_BUFFER_TOO_SMALL = 0x0000047d;
20
+ /** RopId, InputHandleIndex, ReturnValue, Origin and RowCount. */
21
+ const RESPONSE_HEADER_BYTES = 9;
22
+ /**
23
+ * `RopQueryRows` (`[MS-OXCTABL]`/`[MS-OXCROPS]`): fetches up to `RowCount` rows from an already-configured
24
+ * table (`RopGetHierarchyTable` + `RopSetColumns`), advancing the table's cursor. Confirmed field-by-field
25
+ * against `[MS-OXCTABL]`'s own real captured request/response byte example - including the response's
26
+ * `RowData` encoding (`PropertyRow`: a leading `Flags` byte, `0x00` for a `StandardPropertyRow` where every
27
+ * column is a plain `PropertyValue`, `0x01` for a `FlaggedPropertyRow` where each column gets its own
28
+ * `FlaggedPropertyValue` flag+optional-value). This pragmatic subset always emits `StandardPropertyRow`
29
+ * (`Flags = 0x00`) - every configured column always resolves to *some* value here (falling back to a
30
+ * type-appropriate zero/empty default for an unsupported property, never an error), so `FlaggedPropertyRow`'s
31
+ * per-column error signaling is never needed. That "never an error" promise covers a client-requested
32
+ * `propertyType` that doesn't match what a column's own `propertyId` resolves to, not just an entirely
33
+ * unmodeled `propertyId` - `buildRow` writes each column through `PropertyResolvers.writePropertyValueSafely`,
34
+ * which falls back to the same type-appropriate default there too, so one row's mismatched column degrades
35
+ * instead of failing this whole call (and discarding every row already built in it).
36
+ *
37
+ * Backward reads (`ForwardRead = FALSE`) and the `NoAdvance` flag are decoded (to advance the reader
38
+ * correctly) but not honored - this pragmatic subset's tables are simple forward-only cursors. Works generically
39
+ * against either a `RopGetHierarchyTable` (folder rows, `"folder:"`/`"virtual:"` targets) or a
40
+ * `RopGetContentsTable` (message rows, `"message:"` targets) handle - dispatching to `FolderTarget`'s or
41
+ * `MessageTarget`'s resolver and property-value mapping by row-target prefix, since a single table handle only
42
+ * ever holds one kind of row.
43
+ *
44
+ * **Known, deliberate performance limitation**: `resolvePropertyValues` still does one `repo.findOne()` per
45
+ * row (message/calendarEvent/contact/task) rather than a single batched fetch for the whole page - a genuine
46
+ * N+1 for a large `RowCount`. A real fix needs a batched "fetch all these uids" query, which this codebase's
47
+ * `RepoUtils<T>` abstraction doesn't expose a backend-agnostic way to express (Mongo's `$in` and TypeORM's
48
+ * `In()` aren't interchangeable at this call site, which has no idea which backend it's running against) - see
49
+ * `ResolutionCache` for the narrower, safe win this pass *did* make (the redundant *identical* whole-mailbox-
50
+ * folder-list and calendar-mailbox fetches that were happening once per row regardless of which distinct
51
+ * entity each row was, now happen at most once per call).
52
+ *
53
+ * @author Jean-Philippe Steinmetz
54
+ */
55
+ export class RopQueryRowsHandler {
56
+ constructor() {
57
+ this.ropId = ROP_ID_QUERY_ROWS;
58
+ }
59
+ async handle(reader, writer, context) {
60
+ reader.readUInt8(); // LogonId - this pragmatic subset doesn't track multiple concurrent logons per session
61
+ const inputHandleIndex = reader.readUInt8();
62
+ reader.readUInt8(); // QueryRowsFlags - NoAdvance/EnablePackedBuffers not supported; always advances
63
+ reader.readUInt8(); // ForwardRead - backward reads not supported; always reads forward
64
+ const requestedCount = reader.readUInt16LE();
65
+ const table = context.session.handles[inputHandleIndex];
66
+ if (!table || table.type !== "table") {
67
+ writer.writeUInt8(ROP_ID_QUERY_ROWS);
68
+ writer.writeUInt8(inputHandleIndex);
69
+ writer.writeUInt32LE(ERROR_INVALID_OBJECT);
70
+ return;
71
+ }
72
+ const cursor = table.cursor ?? 0;
73
+ const count = Math.min(requestedCount, MAX_ROWS_PER_QUERY);
74
+ // A hierarchy table carries its rows; a contents table reads just this window from the database.
75
+ const slice = table.contentsKind
76
+ ? await resolveContentsWindow(context, table, cursor, count)
77
+ : (table.rows ?? []).slice(cursor, cursor + count);
78
+ // Shared across every row this call resolves - see ResolutionCache's own doc comment for why: without
79
+ // it, a hierarchy table's `hasChildren` column (or a calendar table's organizer/attendee resolution)
80
+ // would re-fetch the same whole-mailbox folder list/mailbox record once per row instead of once per
81
+ // call.
82
+ const cache = {};
83
+ const rowBuffers = [];
84
+ // Only as many rows as fit in the room left in this request's ROP output buffer; the rest stay for the next
85
+ // call, since the cursor only advances past rows actually returned.
86
+ let room = (context.ropOutputRemaining ?? Infinity) - RESPONSE_HEADER_BYTES;
87
+ for (const target of slice) {
88
+ const row = await this.buildRow(context, target, table.columns ?? [], cache);
89
+ if (row.length > room) {
90
+ break;
91
+ }
92
+ room -= row.length;
93
+ rowBuffers.push(row);
94
+ }
95
+ if (rowBuffers.length === 0 && slice.length > 0) {
96
+ // Not even one row fits: [MS-OXCTABL] ecBufferTooSmall, and the cursor stays put.
97
+ writer.writeUInt8(ROP_ID_QUERY_ROWS);
98
+ writer.writeUInt8(inputHandleIndex);
99
+ writer.writeUInt32LE(ERROR_BUFFER_TOO_SMALL);
100
+ return;
101
+ }
102
+ table.cursor = cursor + rowBuffers.length;
103
+ writer.writeUInt8(ROP_ID_QUERY_ROWS);
104
+ writer.writeUInt8(inputHandleIndex);
105
+ writer.writeUInt32LE(0); // ReturnValue - success
106
+ writer.writeUInt8(ORIGIN_BOOKMARK_END);
107
+ writer.writeUInt16LE(rowBuffers.length);
108
+ for (const rowBuffer of rowBuffers) {
109
+ writer.writeBytes(rowBuffer);
110
+ }
111
+ }
112
+ async buildRow(context, target, columns, cache) {
113
+ const writer = new BufferWriter();
114
+ writer.writeUInt8(0x00); // Flags - StandardPropertyRow, see class doc comment
115
+ const values = await resolvePropertyValues(target, columns, context, cache);
116
+ columns.forEach((column, index) => writePropertyValueSafely(writer, column.propertyType, values[index]));
117
+ return writer.toBuffer();
118
+ }
119
+ }
120
+ //# 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,73 @@
1
+ import { loadStreamBody } 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
+ /** RopId, InputHandleIndex, ReturnValue and DataSize. */
16
+ const RESPONSE_HEADER_BYTES = 8;
17
+ /**
18
+ * `RopReadStream` (`[MS-OXCPRPT]`/`[MS-OXCROPS]`): reads up to `ByteCount` (or `MaximumByteCount`, if
19
+ * `ByteCount` is the `0xBABE` sentinel) bytes from an already-opened stream (`RopOpenStream`), advancing the
20
+ * stream's read position - clamped to `MAX_DATA_SIZE` (`0xFFFF`) regardless of what the client asked for, since
21
+ * the response's own `DataSize` field can't carry more than that; a client wanting more of a large body simply
22
+ * calls `RopReadStream` again from the advanced `streamPosition`, the same paging behavior real Exchange uses. It is
23
+ * likewise held to the room left in the request's ROP output buffer (`context.ropOutputRemaining`).
24
+ * Unlike most other ROPs in this pragmatic subset, `[MS-OXCROPS]` documents only one combined response-buffer
25
+ * shape for this ROP (no separate Success/Failure pages) - `DataSize`/`Data` are always present, `DataSize = 0`
26
+ * (empty `Data`) standing in for the failure case rather than the fields being omitted entirely.
27
+ *
28
+ * @author Jean-Philippe Steinmetz
29
+ */
30
+ export class RopReadStreamHandler {
31
+ constructor() {
32
+ this.ropId = ROP_ID_READ_STREAM;
33
+ this.failureTailBytes = 2; // DataSize/WrittenSize
34
+ }
35
+ async handle(reader, writer, context) {
36
+ reader.readUInt8(); // LogonId - this pragmatic subset doesn't track multiple concurrent logons per session
37
+ const inputHandleIndex = reader.readUInt8();
38
+ const byteCount = reader.readUInt16LE();
39
+ const requestedCount = byteCount === BYTE_COUNT_USE_MAXIMUM ? reader.readUInt32LE() : byteCount;
40
+ const handle = context.session.handles[inputHandleIndex];
41
+ if (!handle || handle.type !== "stream") {
42
+ writer.writeUInt8(ROP_ID_READ_STREAM);
43
+ writer.writeUInt8(inputHandleIndex);
44
+ writer.writeUInt32LE(ERROR_INVALID_OBJECT);
45
+ writer.writeUInt16LE(0); // DataSize - see class doc comment on why this is still written on failure
46
+ return;
47
+ }
48
+ const bytes = await loadStreamBody(context, inputHandleIndex, handle);
49
+ const position = handle.streamPosition ?? 0;
50
+ // Also held to the room left in this request's ROP output buffer, after this response's 8 fixed bytes.
51
+ const room = Math.max(0, (context.ropOutputRemaining ?? Infinity) - RESPONSE_HEADER_BYTES);
52
+ const wanted = Math.min(requestedCount, MAX_DATA_SIZE);
53
+ const available = bytes.subarray(position, position + wanted);
54
+ if (room === 0 && available.length > 0) {
55
+ // No room for even one byte: a DataSize of 0 would read as the end of the stream. Answer with what the read
56
+ // needs instead, which doesn't fit, so the dispatcher turns it into RopBufferTooSmall. The position stays.
57
+ writer.writeUInt8(ROP_ID_READ_STREAM);
58
+ writer.writeUInt8(inputHandleIndex);
59
+ writer.writeUInt32LE(0);
60
+ writer.writeUInt16LE(available.length);
61
+ writer.writeBytes(available);
62
+ return;
63
+ }
64
+ const slice = available.subarray(0, room);
65
+ handle.streamPosition = position + slice.length;
66
+ writer.writeUInt8(ROP_ID_READ_STREAM);
67
+ writer.writeUInt8(inputHandleIndex);
68
+ writer.writeUInt32LE(0); // ReturnValue - success
69
+ writer.writeUInt16LE(slice.length); // DataSize
70
+ writer.writeBytes(slice);
71
+ }
72
+ }
73
+ //# sourceMappingURL=RopReadStreamHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RopReadStreamHandler.js","sourceRoot":"","sources":["../../../src/rop/RopReadStreamHandler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { releaseHandle } from "../MapiSessionManager.js";
2
+ const ROP_ID_RELEASE = 0x01;
3
+ /**
4
+ * `RopRelease` (`[MS-OXCROPS]`): releases a Server object handle. Confirmed against the spec's own real
5
+ * 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
6
+ * (`RopId`/`LogonId`/`InputHandleIndex`) and, uniquely among ROPs, **produces no response entry at all** - the
7
+ * server MUST NOT write anything to the output buffer for it, so `handle()` never touches `writer`.
8
+ *
9
+ * @author Jean-Philippe Steinmetz
10
+ */
11
+ export class RopReleaseHandler {
12
+ constructor() {
13
+ this.ropId = ROP_ID_RELEASE;
14
+ this.hasNoResponse = true;
15
+ }
16
+ handle(reader, _writer, context) {
17
+ reader.readUInt8(); // LogonId - this pragmatic subset doesn't track per-logon-id state separately
18
+ const inputHandleIndex = reader.readUInt8();
19
+ // Also releases write streams opened against this handle and drops its cached data - see releaseHandle.
20
+ releaseHandle(context.session, inputHandleIndex);
21
+ }
22
+ }
23
+ //# sourceMappingURL=RopReleaseHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RopReleaseHandler.js","sourceRoot":"","sources":["../../../src/rop/RopReleaseHandler.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAGzD,MAAM,cAAc,GAAG,IAAI,CAAC;AAE5B;;;;;;;GAOG;AACH,MAAM,OAAO,iBAAiB;IAA9B;QACoB,UAAK,GAAG,cAAc,CAAC;QACvB,kBAAa,GAAG,IAAI,CAAC;IAQzC,CAAC;IANU,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,wGAAwG;QACxG,aAAa,CAAC,OAAO,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAC;IACrD,CAAC;CACJ"}
@@ -0,0 +1,269 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import crypto from "crypto";
6
+ import { AttendeeResponseStatus, AttendeeRole, BusyStatus, RecipientType, } from "@rapidmx/restapi";
7
+ import { decodeAppointmentRecurrence } from "../codec/AppointmentRecurrence.js";
8
+ import { BufferReader } from "../codec/BufferCursor.js";
9
+ import { decodeTimeZoneStruct } from "../codec/MapiTimeZone.js";
10
+ import { BUSY_STATUS_FROM_CODE, LID_APPOINTMENT_END_WHOLE, LID_APPOINTMENT_RECUR, LID_APPOINTMENT_START_WHOLE, LID_BUSY_STATUS, LID_LOCATION, LID_REMINDER_DELTA, LID_TIME_ZONE_STRUCT, PSETID_APPOINTMENT, PSETID_COMMON, } from "./CalendarNamedProperties.js";
11
+ import { assignOrGetMid } from "./MessageTarget.js";
12
+ import { resolveNamedProperty } from "./NamedPropertyRegistry.js";
13
+ import { MAX_RECIPIENTS_PER_MESSAGE } from "./AddressList.js";
14
+ import { asEntity, isInvitePending } from "./RestapiRules.js";
15
+ import { parseAddressList } from "./RopSubmitMessageHandler.js";
16
+ const ROP_ID_SAVE_CHANGES_MESSAGE = 0x0c;
17
+ /** The well-known MAPI HRESULT `MAPI_E_INVALID_OBJECT`, reused for "the referenced handle isn't a message (or
18
+ * doesn't exist)" - the same constant `RopGetPropertiesSpecificHandler` uses for its own analogous check. */
19
+ const ERROR_INVALID_OBJECT = 0x80070005;
20
+ /** `MAPI_E_TOO_BIG`: a meeting with more than `MAX_RECIPIENTS_PER_MESSAGE` attendees. */
21
+ const ERROR_TOO_BIG = 0x80040305;
22
+ // The same well-known property IDs RopSetPropertiesHandler tracks - duplicated here (rather than imported)
23
+ // since importing them would only save a handful of literals; see that file for the real documentation of what
24
+ // each means.
25
+ const PID_TAG_SUBJECT = 0x0037;
26
+ const PID_TAG_MESSAGE_CLASS = 0x001a;
27
+ const PID_TAG_DISPLAY_CC = 0x0e03;
28
+ const PID_TAG_DISPLAY_TO = 0x0e04;
29
+ /** `PidTagMessageClass` values starting with this prefix route through `saveAppointment()` instead of the
30
+ * generic draft-MID-assignment path below - see that method's own doc comment. */
31
+ const MESSAGE_CLASS_APPOINTMENT_PREFIX = "IPM.Appointment";
32
+ /**
33
+ * `RopSaveChangesMessage` (`[MS-OXCMSG]`/`[MS-OXCROPS]`): persists a message's pending changes, returning the
34
+ * `MID` a client can reference it by afterwards. **Pragmatic subset**: this library has no separate Drafts-
35
+ * folder persistence step (matching EAS's own `ComposeMailCommand`, which never creates a standalone draft
36
+ * `Message` row either) - saving a `RopCreateMessage` draft does not yet write anything to a database. It only
37
+ * assigns a session-scoped MID (`MessageTarget.assignOrGetMid`, the exact mechanism a `RopQueryRows` row's
38
+ * `PidTagMid` column already uses) so the wire contract's `MessageId` field is well-formed, keyed by this
39
+ * handle's own index (`` `draft:${inputHandleIndex}` ``) since an unsaved draft has no real `"message:<uid>"`
40
+ * target yet. The real work - actually building and sending a MIME message from the properties/body
41
+ * accumulated so far - happens at `RopSubmitMessage` time, not here; calling Save without ever calling Submit
42
+ * silently discards the draft when the session expires, a real, deliberate limitation (this pragmatic subset
43
+ * has no autosave-recovery story) rather than a silently-wrong one.
44
+ *
45
+ * Re-saving an already-real message (a `RopOpenMessage` handle, not a fresh `RopCreateMessage` draft) is
46
+ * accepted but is a no-op beyond echoing that message's own already-assigned MID - editing an existing
47
+ * message's properties is out of scope for this pragmatic subset's compose/send-only ROP coverage.
48
+ *
49
+ * `SaveFlags` is decoded to advance past it correctly but not honored - this pragmatic subset has no
50
+ * conflict-resolution/force-save semantics to vary by flag.
51
+ *
52
+ * **Calendar branch**: unlike mail, a draft whose `PidTagMessageClass` starts with `"IPM.Appointment"` *is*
53
+ * durably persisted here (`saveAppointment()`) - creating (or updating, if this handle already refers to a real
54
+ * `"calendarEvent:<uid>"`) a real `CalendarEvent` row, so a subsequent `RopGetPropertiesSpecific` can read it
55
+ * back before `RopSubmitMessage` is ever called (a calendar item must exist as soon as it's saved, the same way
56
+ * a real Exchange server behaves - unlike a mail draft, which this pragmatic subset never persists at all until
57
+ * Submit). Saving never invites anyone: a saved revision is stamped as already handled (`inviteSequenceSent =
58
+ * sequence`), so restapi's `MeetingSchedulingJob` skips it until `RopSubmitMessageHandler.submitAppointment` asks for
59
+ * invites. Attendees of a submitted meeting are then mailed by that job, which is why a meeting is capped at
60
+ * `MAX_RECIPIENTS_PER_MESSAGE` attendees here. Client-supplied times and reminder minutes that don't parse are ignored (the field keeps its
61
+ * existing or default value) rather than stored as `Invalid Date`/`NaN`, and an update is versioned (`asEntity`).
62
+ *
63
+ * @author Jean-Philippe Steinmetz
64
+ */
65
+ export class RopSaveChangesMessageHandler {
66
+ constructor() {
67
+ this.ropId = ROP_ID_SAVE_CHANGES_MESSAGE;
68
+ }
69
+ async handle(reader, writer, context) {
70
+ reader.readUInt8(); // LogonId - this pragmatic subset doesn't track multiple concurrent logons per session
71
+ const responseHandleIndex = reader.readUInt8();
72
+ const inputHandleIndex = reader.readUInt8();
73
+ reader.readUInt8(); // SaveFlags - see class doc comment
74
+ const handle = context.session.handles[inputHandleIndex];
75
+ if (!handle || handle.type !== "message") {
76
+ writer.writeUInt8(ROP_ID_SAVE_CHANGES_MESSAGE);
77
+ writer.writeUInt8(responseHandleIndex);
78
+ writer.writeUInt32LE(ERROR_INVALID_OBJECT);
79
+ return;
80
+ }
81
+ const draftProperties = handle.draftProperties ?? {};
82
+ const messageClass = draftProperties[String(PID_TAG_MESSAGE_CLASS)] ?? "IPM.Note";
83
+ const mid = messageClass.startsWith(MESSAGE_CLASS_APPOINTMENT_PREFIX)
84
+ ? await this.saveAppointment(handle, draftProperties, context)
85
+ : BigInt(assignOrGetMid(context.session, handle.entityUid !== "" ? handle.entityUid : `draft:${inputHandleIndex}`));
86
+ if (mid === undefined) {
87
+ writer.writeUInt8(ROP_ID_SAVE_CHANGES_MESSAGE);
88
+ writer.writeUInt8(responseHandleIndex);
89
+ writer.writeUInt32LE(ERROR_TOO_BIG);
90
+ return;
91
+ }
92
+ writer.writeUInt8(ROP_ID_SAVE_CHANGES_MESSAGE);
93
+ writer.writeUInt8(responseHandleIndex);
94
+ writer.writeUInt32LE(0); // ReturnValue - success
95
+ writer.writeUInt8(inputHandleIndex);
96
+ writer.writeBigUInt64LE(mid);
97
+ }
98
+ /**
99
+ * Persists (creating, or updating an already-real one) a `CalendarEvent` from the draft's accumulated
100
+ * `RopSetProperties` values, and returns the MID a client can reference it by. Every field this pragmatic
101
+ * subset understands (per `CalendarNamedProperties.ts`) is decoded via `decodeCalendarFieldsFromDraft`
102
+ * below - the write-side mirror of `PropertyResolvers.calendarEventValueFor`'s read-side switch. A field the
103
+ * client never set (e.g. no `RopSetProperties` call touched `PidLidBusyStatus`) falls back to the existing
104
+ * row's own value on an update, or the entity class's own default on a fresh create.
105
+ *
106
+ * Returns `undefined`, saving nothing, for more than `MAX_RECIPIENTS_PER_MESSAGE` attendees: restapi's
107
+ * `MeetingSchedulingJob` mails every attendee of a saved meeting.
108
+ */
109
+ async saveAppointment(handle, draftProperties, context) {
110
+ const decoded = decodeCalendarFieldsFromDraft(context.session, draftProperties);
111
+ if (decoded.attendeeAddresses.length > MAX_RECIPIENTS_PER_MESSAGE) {
112
+ return undefined;
113
+ }
114
+ const attendees = decoded.attendeeAddresses.map((address) => ({
115
+ address,
116
+ role: AttendeeRole.REQUIRED,
117
+ responseStatus: AttendeeResponseStatus.NEEDS_ACTION,
118
+ isOrganizer: false,
119
+ }));
120
+ if (handle.entityUid.startsWith("calendarEvent:")) {
121
+ const uid = handle.entityUid.slice("calendarEvent:".length);
122
+ const existing = await context.calendarEventRepo.findOne(uid, {
123
+ ignoreACL: true,
124
+ });
125
+ if (existing) {
126
+ const updated = {
127
+ uid: existing.uid,
128
+ version: existing.version,
129
+ title: decoded.title ?? existing.title,
130
+ location: decoded.location ?? existing.location,
131
+ startDate: decoded.startDate ?? existing.startDate,
132
+ endDate: decoded.endDate ?? existing.endDate,
133
+ busyStatus: decoded.busyStatus ?? existing.busyStatus,
134
+ recurrenceRule: decoded.recurrenceRule ?? existing.recurrenceRule,
135
+ timezone: decoded.timezone ?? existing.timezone,
136
+ reminderMinutesBeforeStart: decoded.reminderMinutesBeforeStart ?? existing.reminderMinutesBeforeStart,
137
+ // An attendee who is still invited keeps their recorded response.
138
+ attendees: attendees.length > 0
139
+ ? attendees.map((attendee) => existing.attendees.find((prior) => prior.address.toLowerCase() === attendee.address.toLowerCase()) ??
140
+ attendee)
141
+ : existing.attendees,
142
+ };
143
+ const sequence = (existing.sequence ?? 0) + (isSchedulingRelevantChange(existing, updated) ? 1 : 0);
144
+ await context.calendarEventRepo.update({
145
+ ...updated,
146
+ sequence,
147
+ // Saving sends nothing, so this revision counts as handled - unless a submitted revision is still
148
+ // waiting for the job, which then sends the meeting as it now stands rather than not at all.
149
+ inviteSequenceSent: isInvitePending(existing) ? (existing.inviteSequenceSent ?? null) : sequence,
150
+ }, asEntity(context.calendarEventRepo, existing), { ignoreACL: true });
151
+ }
152
+ return BigInt(assignOrGetMid(context.session, handle.entityUid));
153
+ }
154
+ const mailbox = await context.mailboxRepo.findOne(context.mailboxUid, { ignoreACL: true });
155
+ // draftFolderUid is a "folder:<uid>"-prefixed target string (RopCreateMessageHandler stores the same
156
+ // target FolderTarget.assignOrGetFid assigned), not a bare uid - strip the prefix before persisting.
157
+ const draftFolderTarget = handle.draftFolderUid ?? "";
158
+ const folderUid = draftFolderTarget.startsWith("folder:") ? draftFolderTarget.slice("folder:".length) : draftFolderTarget;
159
+ const created = await context.calendarEventRepo.create(new context.calendarEventClass({
160
+ folderUid,
161
+ mailboxUid: context.mailboxUid,
162
+ title: decoded.title ?? "",
163
+ location: decoded.location,
164
+ startDate: decoded.startDate ?? new Date(),
165
+ endDate: decoded.endDate ?? new Date(),
166
+ timezone: decoded.timezone ?? "UTC",
167
+ organizer: { address: mailbox?.primarySmtpAddress ?? "", displayName: mailbox?.displayName, type: RecipientType.TO },
168
+ attendees,
169
+ busyStatus: decoded.busyStatus ?? BusyStatus.BUSY,
170
+ recurrenceRule: decoded.recurrenceRule,
171
+ reminderMinutesBeforeStart: decoded.reminderMinutesBeforeStart,
172
+ icalUid: `${crypto.randomUUID()}@mapi`,
173
+ sequence: 0,
174
+ // Handled until submitted: saving a meeting doesn't invite its attendees.
175
+ inviteSequenceSent: 0,
176
+ }), { ignoreACL: true });
177
+ handle.entityUid = `calendarEvent:${created.uid}`;
178
+ return BigInt(assignOrGetMid(context.session, handle.entityUid));
179
+ }
180
+ }
181
+ /**
182
+ * `true` when an update changes what attendees were invited to: the start or end time, location, recurrence, or
183
+ * who is invited. The same fields restapi's `BaseCalendarEventRoute.update()` bumps `sequence` for (a MAPI save
184
+ * never sets `status`), so an edit made in Outlook is re-sent to attendees and supersedes their copy (RFC 5546
185
+ * `SEQUENCE`) exactly like one made through REST.
186
+ */
187
+ export function isSchedulingRelevantChange(existing, updated) {
188
+ const addresses = (attendees) => JSON.stringify(attendees.map((attendee) => attendee.address.toLowerCase()).sort());
189
+ return (new Date(updated.startDate).getTime() !== new Date(existing.startDate).getTime() ||
190
+ new Date(updated.endDate).getTime() !== new Date(existing.endDate).getTime() ||
191
+ (updated.location ?? "") !== (existing.location ?? "") ||
192
+ JSON.stringify(updated.recurrenceRule ?? null) !== JSON.stringify(existing.recurrenceRule ?? null) ||
193
+ addresses(updated.attendees) !== addresses(existing.attendees));
194
+ }
195
+ /** Decodes a draft message handle's accumulated `RopSetProperties` values (`Record<string, string>`, keyed by
196
+ * decimal `PropertyId`) back into typed `CalendarEvent` fields - the write-side mirror of
197
+ * `PropertyResolvers.calendarEventValueFor`'s read-side switch, sharing the same `CalendarNamedProperties.ts`
198
+ * LID table. `PidTagDisplayTo`/`Cc` (not `Bcc` - a meeting has no concept of a blind attendee) become
199
+ * `attendeeAddresses`, matching `RopSubmitMessageHandler`'s own mail-side addressing pragmatic subset (no
200
+ * `RopModifyRecipients` support - see that file's own doc comment).
201
+ *
202
+ * `title` used to default to `""` whenever `PidTagSubject` wasn't present in `properties` at all, rather than
203
+ * staying `undefined` the way every other optional field here already did (the named-property loop below only
204
+ * ever assigns a field when its key is actually present) - `saveAppointment`'s update branch then wrote that
205
+ * `""` straight through with no existing-value fallback, silently blanking the title of any event edited via a
206
+ * `RopSetProperties` call that only touched some other property (e.g. just `PidLidBusyStatus`). Checking `in`
207
+ * explicitly (rather than `properties[key] ?? ""`) preserves the same "present but genuinely empty" vs.
208
+ * "absent" distinction `PidTagSubject` deserves alongside every other field here. */
209
+ function decodeCalendarFieldsFromDraft(session, properties) {
210
+ const subjectKey = String(PID_TAG_SUBJECT);
211
+ const decoded = {
212
+ title: subjectKey in properties ? properties[subjectKey] : undefined,
213
+ attendeeAddresses: [
214
+ ...parseAddressList(properties[String(PID_TAG_DISPLAY_TO)]),
215
+ ...parseAddressList(properties[String(PID_TAG_DISPLAY_CC)]),
216
+ ],
217
+ };
218
+ for (const [key, value] of Object.entries(properties)) {
219
+ const propertyId = Number(key);
220
+ if (propertyId < 0x8000) {
221
+ continue;
222
+ }
223
+ const named = resolveNamedProperty(session, propertyId);
224
+ if (!named || named.kind !== "lid") {
225
+ continue;
226
+ }
227
+ const guid = named.guid.toLowerCase();
228
+ if (guid === PSETID_APPOINTMENT) {
229
+ switch (named.lid) {
230
+ case LID_LOCATION:
231
+ decoded.location = value;
232
+ break;
233
+ case LID_APPOINTMENT_START_WHOLE:
234
+ decoded.startDate = validDate(value);
235
+ break;
236
+ case LID_APPOINTMENT_END_WHOLE:
237
+ decoded.endDate = validDate(value);
238
+ break;
239
+ case LID_BUSY_STATUS:
240
+ decoded.busyStatus = BUSY_STATUS_FROM_CODE[Number(value)] ?? BusyStatus.BUSY;
241
+ break;
242
+ case LID_APPOINTMENT_RECUR:
243
+ decoded.recurrenceRule = decodeAppointmentRecurrence(new BufferReader(Buffer.from(value, "base64")));
244
+ break;
245
+ case LID_TIME_ZONE_STRUCT:
246
+ decoded.timezone = decodeTimeZoneStruct(new BufferReader(Buffer.from(value, "base64")));
247
+ break;
248
+ default:
249
+ break;
250
+ }
251
+ }
252
+ else if (guid === PSETID_COMMON && named.lid === LID_REMINDER_DELTA) {
253
+ decoded.reminderMinutesBeforeStart = validMinutes(value);
254
+ }
255
+ }
256
+ return decoded;
257
+ }
258
+ /** `value` as a date, or `undefined` (the field is left as it was) when it doesn't parse. A client picks the property
259
+ * type it sends, so a "time" can arrive as any string. */
260
+ function validDate(value) {
261
+ const date = new Date(value);
262
+ return Number.isNaN(date.getTime()) ? undefined : date;
263
+ }
264
+ /** `value` as a whole, non-negative number of minutes, or `undefined` when it isn't one. */
265
+ function validMinutes(value) {
266
+ const minutes = Number(value);
267
+ return value.trim() !== "" && Number.isSafeInteger(minutes) && minutes >= 0 ? minutes : undefined;
268
+ }
269
+ //# sourceMappingURL=RopSaveChangesMessageHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RopSaveChangesMessageHandler.js","sourceRoot":"","sources":["../../../src/rop/RopSaveChangesMessageHandler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import { readPropertyTagArray } from "../codec/PropertyValue.js";
2
+ const ROP_ID_SET_COLUMNS = 0x12;
3
+ /** `TBLSTAT_COMPLETE` - this pragmatic subset's tables are always populated synchronously, so `RopSetColumns`
4
+ * (and every other table ROP) never reports any other `TableStatus` value. */
5
+ const TABLE_STATUS_COMPLETE = 0x00;
6
+ /** The well-known MAPI HRESULT `MAPI_E_INVALID_OBJECT`, reused for "the referenced handle isn't a table (or
7
+ * doesn't exist)" - the same constant `RopQueryRowsHandler`/`RopGetHierarchyTableHandler` use for their own
8
+ * analogous checks. */
9
+ const ERROR_INVALID_OBJECT = 0x80070005;
10
+ /** `MAPI_E_TOO_BIG`, for more than `MAX_PROPERTY_TAG_COUNT` columns. */
11
+ const ERROR_TOO_BIG = 0x80040305;
12
+ /**
13
+ * `RopSetColumns` (`[MS-OXCTABL]`/`[MS-OXCROPS]`): configures which properties a table's subsequent
14
+ * `RopQueryRows` calls return, and in what order. Unlike `RopOpenFolder`/`RopGetHierarchyTable`, this ROP
15
+ * doesn't create a new handle - it mutates the existing table handle referenced by `InputHandleIndex` in
16
+ * place, so its response echoes `InputHandleIndex` rather than an `OutputHandleIndex`, the same convention
17
+ * `RopQueryRows`' own response uses (confirmed against `[MS-OXCTABL]`'s real captured response example).
18
+ *
19
+ * Validates that `InputHandleIndex` actually refers to a `"table"` handle before mutating it, the same check
20
+ * every other handle-consuming ROP in this pragmatic subset already makes - a missing or wrong-type handle
21
+ * previously fell through silently (no `else` branch) to a bare success response, meaning a client that set
22
+ * columns against a stale/released/wrong-kind handle index only discovered the real problem one ROP later,
23
+ * when `RopQueryRows` on that same index returned `MAPI_E_INVALID_OBJECT` instead.
24
+ *
25
+ * @author Jean-Philippe Steinmetz
26
+ */
27
+ export class RopSetColumnsHandler {
28
+ constructor() {
29
+ this.ropId = ROP_ID_SET_COLUMNS;
30
+ }
31
+ 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
+ reader.readUInt8(); // SetColumnsFlags - this pragmatic subset has no async/deferred column-set variant
35
+ const columns = readPropertyTagArray(reader, reader.readUInt16LE());
36
+ const handle = context.session.handles[inputHandleIndex];
37
+ if (!handle || handle.type !== "table" || !columns) {
38
+ writer.writeUInt8(ROP_ID_SET_COLUMNS);
39
+ writer.writeUInt8(inputHandleIndex);
40
+ writer.writeUInt32LE(columns ? ERROR_INVALID_OBJECT : ERROR_TOO_BIG);
41
+ return;
42
+ }
43
+ handle.columns = columns;
44
+ writer.writeUInt8(ROP_ID_SET_COLUMNS);
45
+ writer.writeUInt8(inputHandleIndex);
46
+ writer.writeUInt32LE(0); // ReturnValue - success
47
+ writer.writeUInt8(TABLE_STATUS_COMPLETE);
48
+ }
49
+ }
50
+ //# sourceMappingURL=RopSetColumnsHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RopSetColumnsHandler.js","sourceRoot":"","sources":["../../../src/rop/RopSetColumnsHandler.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAGjE,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAEhC;8EAC8E;AAC9E,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAEnC;;uBAEuB;AACvB,MAAM,oBAAoB,GAAG,UAAU,CAAC;AAExC,wEAAwE;AACxE,MAAM,aAAa,GAAG,UAAU,CAAC;AAEjC;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,oBAAoB;IAAjC;QACoB,UAAK,GAAG,kBAAkB,CAAC;IAsB/C,CAAC;IApBU,MAAM,CAAC,MAAoB,EAAE,MAAoB,EAAE,OAAmB;QACzE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,uFAAuF;QAC3G,MAAM,gBAAgB,GAAW,MAAM,CAAC,SAAS,EAAE,CAAC;QACpD,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,mFAAmF;QACvG,MAAM,OAAO,GAAG,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC;QAEpE,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;QACzD,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC;YACjD,MAAM,CAAC,UAAU,CAAC,kBAAkB,CAAC,CAAC;YACtC,MAAM,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;YACpC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;YACrE,OAAO;QACX,CAAC;QACD,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC;QAEzB,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,UAAU,CAAC,qBAAqB,CAAC,CAAC;IAC7C,CAAC;CACJ"}