@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,54 @@
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 { BufferWriter } from "../codec/BufferCursor.js";
7
+ import { encodeGuid } from "../codec/MapiGuid.js";
8
+ import { resolveCallerMailboxUid } from "@rapidmx/restapi";
9
+ /** `[MS-OXNSPI]` §2.2.1.2's own `MAPI_E_NOT_FOUND` error code, reused for "the caller has no mailbox in this
10
+ * deployment" - the one real failure mode `Bind` can hit in this pragmatic subset. */
11
+ const ERROR_NOT_FOUND = 0x8004010f;
12
+ /**
13
+ * `Bind`/`Unbind` request types (`[MS-OXCMAPIHTTP]` §2.2.5.1/§2.2.5.2, backed by `[MS-OXNSPI]`'s own `NspiBind`/
14
+ * `NspiUnbind` methods): establishes/tears down an NSPI Session Context, the address-book endpoint's own
15
+ * analog of EMSMDB's `Connect`/`Disconnect`.
16
+ *
17
+ * **No real session state is created or tracked** - a deliberate, documented simplification, not an oversight:
18
+ * every subsequent NSPI operation in this pragmatic subset (`GetMatches`) is independently `@Auth(["jwt"])`-
19
+ * protected and re-resolves the caller's own mailbox from that same JWT, exactly the way `RopLogon`/every ROP
20
+ * handler already does for EMSMDB - the real spec's `MapiContext`-equivalent session cookie exists mainly for
21
+ * multi-server-farm request affinity, not additional authentication, so this pragmatic subset mints an opaque,
22
+ * unvalidated cookie value purely for wire-format conformance and never checks it again. `HasState`/`State`
23
+ * (an optional `STAT` scoping the bind to one address-book container) is accepted but not decoded at all -
24
+ * this pragmatic subset has exactly one "container" (a mailbox's own `Contact` list) regardless.
25
+ *
26
+ * @author Jean-Philippe Steinmetz
27
+ */
28
+ export async function handleNspiBind(res, user, mailboxRepo) {
29
+ const mailboxUid = await resolveCallerMailboxUid(mailboxRepo, user);
30
+ if (!mailboxUid) {
31
+ const body = new BufferWriter();
32
+ body.writeUInt32LE(ERROR_NOT_FOUND); // StatusCode - MUST NOT be 0 on failure
33
+ body.writeUInt32LE(0); // AuxiliaryBufferSize
34
+ res.status(200).send(body.toBuffer());
35
+ return;
36
+ }
37
+ res.appendHeader("Set-Cookie", `NspiContext=${crypto.randomUUID()}`); // opaque, unvalidated - see class doc comment
38
+ const body = new BufferWriter();
39
+ body.writeUInt32LE(0); // StatusCode - success
40
+ body.writeUInt32LE(0); // ErrorCode - success
41
+ body.writeBytes(encodeGuid(crypto.randomUUID())); // ServerGuid - a fresh value per Bind is harmless, see class doc comment
42
+ body.writeUInt32LE(0); // AuxiliaryBufferSize
43
+ res.status(200).send(body.toBuffer());
44
+ }
45
+ /** `Unbind` never fails and has nothing to tear down - see `handleNspiBind`'s own doc comment for why no real
46
+ * session exists to release. */
47
+ export function handleNspiUnbind(res) {
48
+ const body = new BufferWriter();
49
+ body.writeUInt32LE(0); // StatusCode
50
+ body.writeUInt32LE(0); // ErrorCode
51
+ body.writeUInt32LE(0); // AuxiliaryBufferSize
52
+ res.status(200).send(body.toBuffer());
53
+ }
54
+ //# sourceMappingURL=NspiBindHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NspiBindHandler.js","sourceRoot":"","sources":["../../../src/nspi/NspiBindHandler.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,MAAM,MAAM,QAAQ,CAAC;AAG5B,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAE3D;sFACsF;AACtF,MAAM,eAAe,GAAG,UAAU,CAAC;AAEnC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAiB,EAAE,IAAa,EAAE,WAA2B;IAC9F,MAAM,UAAU,GAAG,MAAM,uBAAuB,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;IACpE,IAAI,CAAC,UAAU,EAAE,CAAC;QACd,MAAM,IAAI,GAAG,IAAI,YAAY,EAAE,CAAC;QAChC,IAAI,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC,wCAAwC;QAC7E,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,sBAAsB;QAC7C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QACtC,OAAO;IACX,CAAC;IAED,GAAG,CAAC,YAAY,CAAC,YAAY,EAAE,eAAe,MAAM,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC,8CAA8C;IAEpH,MAAM,IAAI,GAAG,IAAI,YAAY,EAAE,CAAC;IAChC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,uBAAuB;IAC9C,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,sBAAsB;IAC7C,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC,yEAAyE;IAC3H,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,sBAAsB;IAC7C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;AAC1C,CAAC;AAED;gCACgC;AAChC,MAAM,UAAU,gBAAgB,CAAC,GAAiB;IAC9C,MAAM,IAAI,GAAG,IAAI,YAAY,EAAE,CAAC;IAChC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa;IACpC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY;IACnC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,sBAAsB;IAC7C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;AAC1C,CAAC"}
@@ -0,0 +1,106 @@
1
+ import { MAX_PROPERTY_TAG_COUNT, PropertyType, readPropertyTag, readPropertyTagArray, readTaggedPropertyValue, writePropertyTag, writePropertyValue, } from "../codec/PropertyValue.js";
2
+ export function readStat(reader) {
3
+ return {
4
+ sortType: reader.readUInt32LE(),
5
+ containerId: reader.readUInt32LE(),
6
+ currentRec: reader.readUInt32LE(),
7
+ delta: reader.readInt32LE(),
8
+ numPos: reader.readUInt32LE(),
9
+ totalRecs: reader.readUInt32LE(),
10
+ codePage: reader.readUInt32LE(),
11
+ templateLocale: reader.readUInt32LE(),
12
+ sortLocale: reader.readUInt32LE(),
13
+ };
14
+ }
15
+ export function writeStat(writer, stat) {
16
+ writer.writeUInt32LE(stat.sortType);
17
+ writer.writeUInt32LE(stat.containerId);
18
+ writer.writeUInt32LE(stat.currentRec);
19
+ writer.writeInt32LE(stat.delta);
20
+ writer.writeUInt32LE(stat.numPos);
21
+ writer.writeUInt32LE(stat.totalRecs);
22
+ writer.writeUInt32LE(stat.codePage);
23
+ writer.writeUInt32LE(stat.templateLocale);
24
+ writer.writeUInt32LE(stat.sortLocale);
25
+ }
26
+ /** A blank `STAT` (`SortType`/`ContainerID`/... all zero) - what this pragmatic subset always echoes back
27
+ * (in `Bind`'s optional acknowledgement and `GetMatches`' response) regardless of the caller's own request
28
+ * `STAT`, since no real per-container/locale state is tracked. */
29
+ export const BLANK_STAT = {
30
+ sortType: 0,
31
+ containerId: 0,
32
+ currentRec: 0,
33
+ delta: 0,
34
+ numPos: 0,
35
+ totalRecs: 0,
36
+ codePage: 0,
37
+ templateLocale: 0,
38
+ sortLocale: 0,
39
+ };
40
+ /** `LargePropertyTagArray` (`[MS-OXCMAPIHTTP]` §2.2.1.8): a 4-byte count followed by that many `PropertyTag`
41
+ * structures (`[MS-OXCDATA]` §2.9 - the same `PropertyType`-then-`PropertyId` encoding `PropertyValue.ts`'s own
42
+ * `readPropertyTag`/`writePropertyTag` already implement). */
43
+ export function readLargePropertyTagArray(reader) {
44
+ const tags = readPropertyTagArray(reader, reader.readUInt32LE());
45
+ if (!tags) {
46
+ // Every requested column is resolved for every returned row, so the count is capped like EMSMDB's.
47
+ throw new RangeError(`NspiCodec: a LargePropertyTagArray may hold at most ${MAX_PROPERTY_TAG_COUNT} tags.`);
48
+ }
49
+ return tags;
50
+ }
51
+ export function writeLargePropertyTagArray(writer, tags) {
52
+ writer.writeUInt32LE(tags.length);
53
+ for (const tag of tags) {
54
+ writePropertyTag(writer, tag);
55
+ }
56
+ }
57
+ /** Writes one column's `AddressBookPropertyValue` (`[MS-OXCMAPIHTTP]` §2.2.1.1): a `PtypString`/`PtypString8`/
58
+ * `PtypBinary` value is preceded by a `HasValue` byte (always `0xFF`/`TRUE` in this pragmatic subset, which
59
+ * never reports a missing column value - the same "always emit a real value of the right type" principle
60
+ * `PropertyResolvers.defaultValueForType` already applies for EMSMDB); every other (fixed-size) type has no
61
+ * such prefix. `PtypMultiple*` columns (which need a `HasValue` byte per *element*, not just once) are never
62
+ * produced by this pragmatic subset's own fixed GAL column set and are intentionally not handled here. */
63
+ function writeAddressBookPropertyValue(writer, type, value) {
64
+ if (type === PropertyType.PtypString || type === PropertyType.PtypString8 || type === PropertyType.PtypBinary) {
65
+ writer.writeUInt8(0xff); // HasValue - always TRUE, see function doc comment
66
+ }
67
+ writePropertyValue(writer, type, value);
68
+ }
69
+ /** Writes one `AddressBookPropertyRow` (`[MS-OXCMAPIHTTP]` §2.2.1.7) - always `Flags=0x00` (every column
70
+ * value present without error), the exact `StandardPropertyRow`-equivalent choice `RopQueryRowsHandler`/
71
+ * `RopGetPropertiesSpecificHandler` already make for EMSMDB's own row encoding. */
72
+ export function writeAddressBookPropertyRow(writer, columns, values) {
73
+ writer.writeUInt8(0x00); // Flags - see function doc comment
74
+ columns.forEach((column, index) => writeAddressBookPropertyValue(writer, column.propertyType, values[index]));
75
+ }
76
+ /** Decodes a `Restriction` (`[MS-OXCDATA]` §2.12)'s leading `RestrictType` byte and, only for a
77
+ * `ContentRestriction`/`RES_CONTENT` (`0x03`) - the shape a real client's own GAL "search as you type" sends -
78
+ * extracts the plain string search term from its `TaggedValue` field.
79
+ *
80
+ * **Throws for any other restriction type** (`AND`/`OR`/`PropertyRestriction`/...) rather than degrading
81
+ * gracefully - unlike this pragmatic subset's usual "return a default instead of failing" stance elsewhere,
82
+ * a restriction's own byte length is type-dependent (12 different packet formats, several recursively nested),
83
+ * so once an unrecognized `RestrictType` is seen there is no safe way to know how many bytes to skip to reach
84
+ * the request body's own subsequent fields (`RowCount`/`Columns`/...) - silently returning `undefined` here
85
+ * would leave the reader mid-structure and corrupt every field decoded afterward. Throwing is the honest,
86
+ * safe choice for an input this narrow codec was never meant to parse, not a design gap.
87
+ *
88
+ * `FuzzyLevelLow`/`FuzzyLevelHigh` (exact/substring/prefix matching, case sensitivity) and the restriction's
89
+ * own target `PropertyTag` are decoded to advance the reader correctly but not honored - this pragmatic
90
+ * subset always does a case-insensitive substring match against its own fixed Contact field set regardless of
91
+ * which property or fuzzy level the client's restriction actually named. */
92
+ export function extractContentRestrictionSearchTerm(reader) {
93
+ const restrictType = reader.readUInt8();
94
+ if (restrictType !== 0x03) {
95
+ throw new Error(`NspiCodec: unsupported restriction type 0x${restrictType.toString(16)} (only ContentRestriction/RES_CONTENT is supported)`);
96
+ }
97
+ reader.readUInt16LE(); // FuzzyLevelLow - not honored, see function doc comment
98
+ reader.readUInt16LE(); // FuzzyLevelHigh - not honored, see function doc comment
99
+ readPropertyTag(reader); // PropertyTag (the restriction's own target column) - not honored
100
+ const tagged = readTaggedPropertyValue(reader);
101
+ if (typeof tagged.value !== "string") {
102
+ throw new Error("NspiCodec: ContentRestriction's TaggedValue was not a string.");
103
+ }
104
+ return tagged.value;
105
+ }
106
+ //# sourceMappingURL=NspiCodec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NspiCodec.js","sourceRoot":"","sources":["../../../src/nspi/NspiCodec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,142 @@
1
+ import { boundedEscapedPattern } from "../RegexPatternUtils.js";
2
+ import { BufferReader, BufferWriter } from "../codec/BufferCursor.js";
3
+ import { PropertyType } from "../codec/PropertyValue.js";
4
+ import { defaultValueForType } from "../rop/PropertyResolvers.js";
5
+ import { BLANK_STAT, extractContentRestrictionSearchTerm, readLargePropertyTagArray, readStat, writeAddressBookPropertyRow, writeLargePropertyTagArray, writeStat, } from "./NspiCodec.js";
6
+ /** `PidTagDisplayName`/`PidTagEmailAddress` - the small default column set this pragmatic subset returns when
7
+ * a client's `GetMatches` request omits its own `Columns` field, mirroring EAS's own GAL result shape
8
+ * (`SearchCommand.ts`'s `DisplayName`/`EmailAddress`). */
9
+ const PID_TAG_DISPLAY_NAME = 0x3001;
10
+ const PID_TAG_EMAIL_ADDRESS = 0x3003;
11
+ /** The most rows one `GetMatches` lookup asks the repo for per query - `RepoUtils.find()`'s own page cap. */
12
+ export const MAX_MATCH_ROWS = 1000;
13
+ const DEFAULT_COLUMNS = [
14
+ { propertyId: PID_TAG_DISPLAY_NAME, propertyType: PropertyType.PtypString },
15
+ { propertyId: PID_TAG_EMAIL_ADDRESS, propertyType: PropertyType.PtypString },
16
+ ];
17
+ /** A small, non-cryptographic FNV-1a 32-bit hash of a `Contact.uid`, used as that contact's `MinimalEntryID`
18
+ * in a `GetMatches` response. Deterministic per `uid` but not persisted or globally coordinated - acceptable
19
+ * here since no other operation in this pragmatic subset ever looks a `MinimalEntryID` back up (`NspiGetProps`/
20
+ * `NspiQueryRows`/... are out of scope), so nothing depends on it staying stable beyond a single response. */
21
+ function minimalEntryIdFor(uid) {
22
+ let hash = 0x811c9dc5;
23
+ for (let i = 0; i < uid.length; i++) {
24
+ hash ^= uid.charCodeAt(i);
25
+ hash = Math.imul(hash, 0x01000193);
26
+ }
27
+ return hash >>> 0;
28
+ }
29
+ function valueForColumn(contact, column) {
30
+ switch (column.propertyId) {
31
+ case PID_TAG_DISPLAY_NAME:
32
+ return contact.displayName;
33
+ case PID_TAG_EMAIL_ADDRESS:
34
+ // Optional-chained on `emails` itself, not just its first element - a Contact predating this field
35
+ // (or otherwise missing it) would throw on a bare `contact.emails[0]` before the `?.` on `.address`
36
+ // ever gets a chance to help.
37
+ return contact.emails?.[0]?.address ?? "";
38
+ default:
39
+ return defaultValueForType(column.propertyType);
40
+ }
41
+ }
42
+ /** Finds every `Contact` in `mailboxUid` matching `searchTerm` (a case-insensitive substring match against
43
+ * `displayName`/`givenName`/`surname`/`company`, queried per-field and merged in memory - the same two-backend
44
+ * workaround `SearchCommand.ts` already established for its own identical `$or`-is-Mongo-only /
45
+ * `ILike()`-needs-wrapping gaps) - or every `Contact` in the mailbox at all when `searchTerm` is `undefined`
46
+ * (no filter was supplied, or (`extractContentRestrictionSearchTerm`'s own contract) the filter was already
47
+ * known-good by the time this is called).
48
+ *
49
+ * Uses `RepoUtils`'s `regex(...)` operator (`@rapidrest/service-core` ^2.0), not `like(...)`: `like()` compiles
50
+ * a **glob** pattern (`*`/`?` as wildcards), which would require wrapping the term in `*...*` to get a substring
51
+ * match and would still treat a literal `*`/`?` in the search term itself as a wildcard (glob syntax has no
52
+ * escape for those two characters). `regex()` takes a real, already-anchoring-free regular expression compiled
53
+ * case-insensitively on both backends (`$regex`/`~*`/`REGEXP`), so escaping the term with `StringUtils
54
+ * .escapeRegExp` here gives a genuine literal-substring match with no residual wildcard ambiguity. `regex()`
55
+ * patterns are also independently validated by the framework itself (`ModelUtils.isUnsafeRegexPattern`) against
56
+ * catastrophic-backtracking shapes, but that guard is for patterns a caller assembles from parts; escaping the
57
+ * entire term with `escapeRegExp` before it ever reaches here already prevents any of those shapes from forming
58
+ * in the first place, since no unescaped metacharacter survives. */
59
+ async function findMatchingContacts(mailboxUid, contactRepo, searchTerm, limit) {
60
+ // `limit` goes in both the query (what the SQL query builder reads) and the options (what Mongo reads);
61
+ // without it each backend silently falls back to its own 100-row default regardless of `RowCount`.
62
+ if (!searchTerm) {
63
+ return contactRepo.find({ mailboxUid, limit }, { ignoreACL: true, limit });
64
+ }
65
+ // Bounded so the escaped operand never trips service-core's regex length guard, which would fail the whole
66
+ // NSPI call with INVALID_REQUEST for a long or metacharacter-heavy search term.
67
+ const pattern = boundedEscapedPattern(searchTerm);
68
+ const perField = await Promise.all(["displayName", "givenName", "surname", "company"].map((field) => contactRepo.find({ mailboxUid, [field]: `regex(${pattern})`, limit }, { ignoreACL: true, limit })));
69
+ const byUid = new Map();
70
+ for (const contact of perField.flat()) {
71
+ byUid.set(contact.uid, contact);
72
+ }
73
+ return Array.from(byUid.values()).sort((a, b) => a.displayName.localeCompare(b.displayName));
74
+ }
75
+ /**
76
+ * `GetMatches` request type (`[MS-OXCMAPIHTTP]` §2.2.5.5, backed by `[MS-OXNSPI]`'s own `NspiGetMatches`
77
+ * method): searches the GAL (this library's `Contact` records, the same source of truth EAS's own `Search`
78
+ * command already uses) for entries matching the client's restriction, returning up to `RowCount` matches as
79
+ * `AddressBookPropertyRow`s. This is the real operation behind a client's own address-book "search as you
80
+ * type" UI - the closest real NSPI equivalent to this plan's own "minimal prefix/substring lookups" scope.
81
+ *
82
+ * `HasState`/`State` (an input `STAT` scoping the search and reporting position) and `HasMinimalIds`/
83
+ * `MinimalIds` (an "Explicit Table" restricting the search to a caller-supplied candidate set) are decoded to
84
+ * advance the reader correctly but not honored - this pragmatic subset always searches the caller's entire
85
+ * Contact list fresh on every call (no real per-container/table state, see `NspiBindHandler.ts`'s own doc
86
+ * comment for the same reasoning applied to `Bind`). `HasFilter`'s `Filter` is decoded via
87
+ * `NspiCodec.extractContentRestrictionSearchTerm` - see that function's own doc comment for why an
88
+ * unsupported restriction type throws rather than degrading. `HasPropertyName` (an alternate open-by-named-
89
+ * property addressing mode) is decoded to advance the reader correctly but never itself used to select a
90
+ * search field.
91
+ *
92
+ * The response's own `State` is always `BLANK_STAT` (with `TotalRecs` set to the real match count) - no real
93
+ * per-container `STAT` is tracked, matching this pragmatic subset's stated scope.
94
+ *
95
+ * @author Jean-Philippe Steinmetz
96
+ */
97
+ export async function handleNspiGetMatches(req, res, mailboxUid, contactRepo) {
98
+ const reader = new BufferReader(req.rawBody ?? Buffer.alloc(0));
99
+ reader.readUInt32LE(); // Reserved
100
+ if (reader.readUInt8()) {
101
+ readStat(reader); // State - not honored, see function doc comment
102
+ }
103
+ if (reader.readUInt8()) {
104
+ // MinimalIds (an Explicit Table) - not honored, see function doc comment
105
+ // Skipped in one bounds-checked read rather than a loop over a client-supplied 32-bit count.
106
+ reader.readBytes(reader.readUInt32LE() * 4);
107
+ }
108
+ reader.readUInt32LE(); // InterfaceOptionFlags - reserved, always 0
109
+ const searchTerm = reader.readUInt8() ? extractContentRestrictionSearchTerm(reader) : undefined;
110
+ if (reader.readUInt8()) {
111
+ reader.readBytes(16); // PropertyNameGuid - not honored, see function doc comment
112
+ reader.readUInt32LE(); // PropertyNameId
113
+ }
114
+ const rowCount = reader.readUInt32LE();
115
+ const columns = reader.readUInt8() ? readLargePropertyTagArray(reader) : DEFAULT_COLUMNS;
116
+ // AuxiliaryBufferSize/AuxiliaryBuffer intentionally left unread - no auxiliary-payload support.
117
+ const matches = await findMatchingContacts(mailboxUid, contactRepo, searchTerm, Math.min(Math.max(rowCount, 1), MAX_MATCH_ROWS));
118
+ const page = matches.slice(0, rowCount);
119
+ const body = new BufferWriter();
120
+ body.writeUInt32LE(0); // StatusCode - success
121
+ body.writeUInt32LE(0); // ErrorCode - success
122
+ body.writeUInt8(1); // HasState
123
+ writeStat(body, { ...BLANK_STAT, totalRecs: matches.length });
124
+ body.writeUInt8(page.length > 0 ? 1 : 0); // HasMinimalIds
125
+ if (page.length > 0) {
126
+ body.writeUInt32LE(page.length);
127
+ for (const contact of page) {
128
+ body.writeUInt32LE(minimalEntryIdFor(contact.uid));
129
+ }
130
+ }
131
+ body.writeUInt8(page.length > 0 ? 1 : 0); // HasColumnsAndRows
132
+ if (page.length > 0) {
133
+ writeLargePropertyTagArray(body, columns);
134
+ body.writeUInt32LE(page.length);
135
+ for (const contact of page) {
136
+ writeAddressBookPropertyRow(body, columns, columns.map((column) => valueForColumn(contact, column)));
137
+ }
138
+ }
139
+ body.writeUInt32LE(0); // AuxiliaryBufferSize
140
+ res.status(200).send(body.toBuffer());
141
+ }
142
+ //# sourceMappingURL=NspiGetMatchesHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NspiGetMatchesHandler.js","sourceRoot":"","sources":["../../../src/nspi/NspiGetMatchesHandler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,114 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import addressparser from "nodemailer/lib/addressparser/index.js";
6
+ import { literalQueryValue } from "./RestapiRules.js";
7
+ /** Splits a `PidTagDisplayTo`/`Cc`/`Bcc`-style string on the semicolons Outlook separates recipients with,
8
+ * trimming and dropping empty entries. Commas are not separators: a resolved recipient's display name commonly
9
+ * contains one (`Doe, Jane <jane@example.com>`). */
10
+ export function splitAddressList(value) {
11
+ if (!value) {
12
+ return [];
13
+ }
14
+ return value
15
+ .split(";")
16
+ .map((s) => s.trim())
17
+ .filter((s) => s.length > 0);
18
+ }
19
+ /** A bare `local@domain.tld` address: no whitespace (so no CR/LF), display name, angle brackets, quoting or
20
+ * separators. */
21
+ const PLAIN_EMAIL_ADDRESS = /^[^\s@<>()[\]",;:\\]+@[^\s@<>()[\]",;:\\]+\.[^\s@<>()[\]",;:\\.]+$/;
22
+ /** `true` for a plain SMTP address (see `PLAIN_EMAIL_ADDRESS`) no longer than RFC 5321's 254 characters. These
23
+ * strings reach MIME headers, the SMTP envelope and iCalendar `mailto:` lines, where a CR/LF would inject content. */
24
+ export function isPlainEmailAddress(value) {
25
+ return value.length <= 254 && PLAIN_EMAIL_ADDRESS.test(value);
26
+ }
27
+ /** Control characters (CR/LF included) are replaced in display names; nodemailer encodes the rest. */
28
+ function cleanName(name) {
29
+ return [...name].map((char) => (char.charCodeAt(0) < 0x20 || char.charCodeAt(0) === 0x7f ? " " : char)).join("").replace(/ +/g, " ").trim();
30
+ }
31
+ /**
32
+ * Parses one `;`-separated entry with nodemailer's `addressparser`: `jane@example.com`, `Jane <jane@example.com>`,
33
+ * `"Doe, Jane" <jane@example.com>` or a bare display name `Jane Doe`. An entry holding several addresses yields one
34
+ * recipient each. Any address that isn't a plain SMTP address marks the entry invalid.
35
+ */
36
+ export function parseRecipientEntry(entry) {
37
+ const parts = addressparser(entry, { flatten: true });
38
+ const addressed = parts.filter((part) => part.address);
39
+ if (addressed.length === 0) {
40
+ const name = cleanName(entry);
41
+ // Something address-shaped that addressparser couldn't take apart isn't a display name to look up.
42
+ return [/[@<>]/.test(name) ? { name, invalid: true } : { name }];
43
+ }
44
+ const nameOnlyParts = parts.filter((part) => !part.address && part.name).map((part) => part.name);
45
+ return addressed.map((part) => {
46
+ const name = cleanName(addressed.length === 1 ? [...nameOnlyParts, part.name].filter(Boolean).join(", ") : part.name);
47
+ const address = String(part.address);
48
+ return isPlainEmailAddress(address) && !/[\r\n]/.test(entry) ? { name, address } : { name, invalid: true };
49
+ });
50
+ }
51
+ /** Every entry of a `PidTagDisplayTo`/`Cc`/`Bcc`-style string - see `parseRecipientEntry`. */
52
+ export function parseRecipientList(value) {
53
+ return splitAddressList(value).flatMap(parseRecipientEntry);
54
+ }
55
+ /** The plain addresses in a `PidTagDisplayTo`/`Cc`/`Bcc`-style string, including those written as
56
+ * `Name <address>`, skipping display names without an address and anything invalid. */
57
+ export function parseAddressList(value) {
58
+ return parseRecipientList(value)
59
+ .filter((entry) => entry.address !== undefined)
60
+ .map((entry) => entry.address);
61
+ }
62
+ /** The most contacts looked at when resolving one display name - two is enough to tell "exactly one" apart. */
63
+ const MAX_NAME_MATCHES = 2;
64
+ /** The most recipients (To, Cc and Bcc together) one submitted message or meeting may have - the same limit
65
+ * `@rapidmx/activesync`'s `ComposeMailCommand` enforces (`MAX_COMPOSE_RECIPIENTS`). */
66
+ export const MAX_RECIPIENTS_PER_MESSAGE = 500;
67
+ /** The caller's contacts whose display name is exactly `name`. The name comes from the client, so it is matched
68
+ * literally (`literalQueryValue`) and re-checked on the returned rows; a name the query layer rejects matches nothing.
69
+ * Charged to `budget` like every other single-query lookup (`resolveContentsKind`, `RopDeleteFolderHandler`'s own
70
+ * per-item delete) before the real DB round trip runs - a bare display name is exactly what a client crafting a
71
+ * malicious draft controls, and `RopSubmitMessageHandler` calls this once per unresolved To/Cc/Bcc entry (up to
72
+ * `MAX_RECIPIENTS_PER_MESSAGE` each), so without this charge a large recipient list would run unmetered queries. */
73
+ async function findContactsByDisplayName(context, name) {
74
+ if (!context.contactRepo) {
75
+ return [];
76
+ }
77
+ context.budget?.chargeQueries();
78
+ try {
79
+ const rows = await context.contactRepo.find({ mailboxUid: context.mailboxUid, displayName: literalQueryValue(name), limit: MAX_NAME_MATCHES }, { ignoreACL: true, limit: MAX_NAME_MATCHES });
80
+ return rows.filter((row) => row.displayName === name);
81
+ }
82
+ catch {
83
+ return [];
84
+ }
85
+ }
86
+ /**
87
+ * Resolves a display string into addresses. An entry carrying an address is used as is; a bare display name (Outlook
88
+ * writes names, not addresses, into `PidTagDisplayTo` once it has resolved a recipient against an address book) is
89
+ * looked up among the caller's own contacts by exact display name and used when exactly one contact matches, with
90
+ * that contact's first valid email address.
91
+ */
92
+ export async function resolveRecipientList(value, context) {
93
+ const resolution = { recipients: [], unresolved: [], invalid: [] };
94
+ for (const entry of parseRecipientList(value)) {
95
+ if (entry.invalid) {
96
+ resolution.invalid.push(entry.name);
97
+ }
98
+ else if (entry.address) {
99
+ resolution.recipients.push({ name: entry.name, address: entry.address });
100
+ }
101
+ else {
102
+ const contacts = await findContactsByDisplayName(context, entry.name);
103
+ const address = contacts.length === 1 ? contacts[0].emails?.map((email) => email.address).find(isPlainEmailAddress) : undefined;
104
+ if (address) {
105
+ resolution.recipients.push({ name: entry.name, address });
106
+ }
107
+ else {
108
+ resolution.unresolved.push(entry.name);
109
+ }
110
+ }
111
+ }
112
+ return resolution;
113
+ }
114
+ //# sourceMappingURL=AddressList.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AddressList.js","sourceRoot":"","sources":["../../../src/rop/AddressList.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import { BusyStatus } from "@rapidmx/restapi";
2
+ import { findAllCapped, MAX_COLLECTION_ROWS } from "./RepoPaging.js";
3
+ /** Degrades to empty-looking values for a `"calendarEvent:<uid>"` target whose real `CalendarEvent` has since
4
+ * vanished (soft-deleted or otherwise) - the same "don't fail the whole ROP over one stale row" principle
5
+ * `MessageTarget.resolveMessageInfo`/`FolderTarget.resolveFolderInfo` already apply. */
6
+ export async function resolveCalendarEventInfo(target, calendarEventRepo) {
7
+ const uid = target.slice("calendarEvent:".length);
8
+ const event = await calendarEventRepo.findOne(uid, { ignoreACL: true });
9
+ return {
10
+ title: event?.title ?? "",
11
+ location: event?.location,
12
+ startDate: event?.startDate ?? new Date(0),
13
+ endDate: event?.endDate ?? new Date(0),
14
+ timezone: event?.timezone ?? "UTC",
15
+ busyStatus: event?.busyStatus ?? BusyStatus.BUSY,
16
+ recurrenceRule: event?.recurrenceRule,
17
+ reminderMinutesBeforeStart: event?.reminderMinutesBeforeStart,
18
+ organizerAddress: event?.organizer?.address ?? "",
19
+ attendees: event?.attendees ?? [],
20
+ };
21
+ }
22
+ /** The order a calendar folder's events are listed in: latest start first, ties broken by `uid`. */
23
+ export const CALENDAR_EVENT_SORT = { startDate: "DESC", uid: "ASC" };
24
+ /** Resolves the calendar events directly in `folderUid`, latest first and capped at `MAX_COLLECTION_ROWS`, as
25
+ * `"calendarEvent:<uid>"` target strings. */
26
+ export async function resolveFolderCalendarEvents(folderUid, calendarEventRepo, budget) {
27
+ const { items: events } = await findAllCapped(calendarEventRepo, { folderUid }, CALENDAR_EVENT_SORT, MAX_COLLECTION_ROWS, budget);
28
+ return events.map((e) => `calendarEvent:${e.uid}`);
29
+ }
30
+ //# sourceMappingURL=CalendarEventTarget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CalendarEventTarget.js","sourceRoot":"","sources":["../../../src/rop/CalendarEventTarget.ts"],"names":[],"mappings":"AAKA,OAAO,EAAY,UAAU,EAAiC,MAAM,kBAAkB,CAAC;AAEvF,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAiB,MAAM,iBAAiB,CAAC;AAuBpF;;wFAEwF;AACxF,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,MAAc,EAAE,iBAAiC;IAC5F,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAClD,MAAM,KAAK,GAA8B,MAAM,iBAAiB,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACnG,OAAO;QACH,KAAK,EAAE,KAAK,EAAE,KAAK,IAAI,EAAE;QACzB,QAAQ,EAAE,KAAK,EAAE,QAAQ;QACzB,SAAS,EAAE,KAAK,EAAE,SAAS,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC;QAC1C,OAAO,EAAE,KAAK,EAAE,OAAO,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC;QACtC,QAAQ,EAAE,KAAK,EAAE,QAAQ,IAAI,KAAK;QAClC,UAAU,EAAE,KAAK,EAAE,UAAU,IAAI,UAAU,CAAC,IAAI;QAChD,cAAc,EAAE,KAAK,EAAE,cAAc;QACrC,0BAA0B,EAAE,KAAK,EAAE,0BAA0B;QAC7D,gBAAgB,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,IAAI,EAAE;QACjD,SAAS,EAAE,KAAK,EAAE,SAAS,IAAI,EAAE;KACpC,CAAC;AACN,CAAC;AAED,oGAAoG;AACpG,MAAM,CAAC,MAAM,mBAAmB,GAAa,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;AAE/E;6CAC6C;AAC7C,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAAC,SAAiB,EAAE,iBAAiC,EAAE,MAAsB;IAC1H,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAgB,iBAAiB,EAAE,EAAE,SAAS,EAAE,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,CAAC,CAAC;IACjJ,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;AACvD,CAAC"}
@@ -0,0 +1,57 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { AttendeeResponseStatus, BusyStatus } from "@rapidmx/restapi";
6
+ /**
7
+ * The Calendar named-property identity table (`(PropertySet GUID, LID)` pairs) this pragmatic subset supports,
8
+ * per `[MS-OXPROPS]` - each pinned against its own MS-OXPROPS page (not invented), shared by `PropertyResolvers.ts`
9
+ * (the read side: `calendarEventValueFor`) and `RopSaveChangesMessageHandler.ts` (the write side: decoding a
10
+ * draft's accumulated `RopSetProperties` values back into a real `CalendarEvent`) so both directions of this
11
+ * mapping stay in exactly one place. See the architecture plan's "Calendar support" section for the full table.
12
+ */
13
+ export const PSETID_APPOINTMENT = "00062002-0000-0000-c000-000000000046";
14
+ export const PSETID_COMMON = "00062008-0000-0000-c000-000000000046";
15
+ export const PSETID_MEETING = "6ed8da90-450b-101b-98da-00aa003f1305";
16
+ export const LID_LOCATION = 0x8208;
17
+ export const LID_APPOINTMENT_START_WHOLE = 0x820d;
18
+ export const LID_APPOINTMENT_END_WHOLE = 0x820e;
19
+ export const LID_BUSY_STATUS = 0x8205;
20
+ export const LID_RECURRING = 0x8223;
21
+ export const LID_REMINDER_SET = 0x8503;
22
+ export const LID_REMINDER_DELTA = 0x8501;
23
+ export const LID_RESPONSE_STATUS = 0x8218;
24
+ export const LID_APPOINTMENT_RECUR = 0x8216;
25
+ export const LID_TIME_ZONE_STRUCT = 0x8233;
26
+ /** `PidLidGlobalObjectId` (`PSETID_Meeting`) - see `GlobalObjectId.ts` for the BLOB it carries and
27
+ * `MeetingMessageClassHandler.ts` for how a meeting response's own copy of it is correlated back to the
28
+ * original `CalendarEvent`. */
29
+ export const LID_GLOBAL_OBJECT_ID = 0x00000003;
30
+ /** `PidLidBusyStatus`'s wire values (`[MS-OXOCAL]` §2.2.1.2, confirmed this session) - a different numbering
31
+ * than `CalendarSyncAdapter`'s own `BUSY_STATUS_CODES` table for the unrelated MS-ASCAL `BusyStatus` field.
32
+ * `olWorkingElsewhere` (0x00000004) has no equivalent in this library's own `BusyStatus` enum - never produced,
33
+ * and decoded (via `BUSY_STATUS_FROM_CODE`) with a fallback to `BUSY`, a documented, harmless approximation. */
34
+ export const BUSY_STATUS_CODES = {
35
+ [BusyStatus.FREE]: 0,
36
+ [BusyStatus.TENTATIVE]: 1,
37
+ [BusyStatus.BUSY]: 2,
38
+ [BusyStatus.OUT_OF_OFFICE]: 3,
39
+ };
40
+ export const BUSY_STATUS_FROM_CODE = {
41
+ 0: BusyStatus.FREE,
42
+ 1: BusyStatus.TENTATIVE,
43
+ 2: BusyStatus.BUSY,
44
+ 3: BusyStatus.OUT_OF_OFFICE,
45
+ };
46
+ /** `PidLidResponseStatus`'s wire values (`[MS-OXOCAL]` §2.2.1.11, confirmed this session) for the caller's own
47
+ * attendee record. `respOrganized` (0x00000001) is returned when the caller *is* the organizer - not a value
48
+ * `AttendeeResponseStatus` itself has a case for, since that enum only models an attendee's own response. */
49
+ export const RESPONSE_STATUS_CODES = {
50
+ [AttendeeResponseStatus.NEEDS_ACTION]: 5, // respNotResponded
51
+ [AttendeeResponseStatus.TENTATIVE]: 2, // respTentative
52
+ [AttendeeResponseStatus.ACCEPTED]: 3, // respAccepted
53
+ [AttendeeResponseStatus.DECLINED]: 4, // respDeclined
54
+ };
55
+ export const RESPONSE_STATUS_ORGANIZED = 1; // respOrganized
56
+ export const RESPONSE_STATUS_NONE = 0; // respNone
57
+ //# sourceMappingURL=CalendarNamedProperties.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CalendarNamedProperties.js","sourceRoot":"","sources":["../../../src/rop/CalendarNamedProperties.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEtE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,sCAAsC,CAAC;AACzE,MAAM,CAAC,MAAM,aAAa,GAAG,sCAAsC,CAAC;AACpE,MAAM,CAAC,MAAM,cAAc,GAAG,sCAAsC,CAAC;AAErE,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC;AACnC,MAAM,CAAC,MAAM,2BAA2B,GAAG,MAAM,CAAC;AAClD,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAM,CAAC;AAChD,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC;AACtC,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC;AACpC,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC;AACvC,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC;AACzC,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAC1C,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC;AAC5C,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAE3C;;+BAE+B;AAC/B,MAAM,CAAC,MAAM,oBAAoB,GAAG,UAAU,CAAC;AAE/C;;;gHAGgH;AAChH,MAAM,CAAC,MAAM,iBAAiB,GAA+B;IACzD,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;IACpB,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;IACzB,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;IACpB,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;CAChC,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAA+B;IAC7D,CAAC,EAAE,UAAU,CAAC,IAAI;IAClB,CAAC,EAAE,UAAU,CAAC,SAAS;IACvB,CAAC,EAAE,UAAU,CAAC,IAAI;IAClB,CAAC,EAAE,UAAU,CAAC,aAAa;CAC9B,CAAC;AAEF;;6GAE6G;AAC7G,MAAM,CAAC,MAAM,qBAAqB,GAA2C;IACzE,CAAC,sBAAsB,CAAC,YAAY,CAAC,EAAE,CAAC,EAAE,mBAAmB;IAC7D,CAAC,sBAAsB,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,gBAAgB;IACvD,CAAC,sBAAsB,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,eAAe;IACrD,CAAC,sBAAsB,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,eAAe;CACxD,CAAC;AACF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,CAAC,gBAAgB;AAC5D,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,CAAC,WAAW"}
@@ -0,0 +1,19 @@
1
+ /** Degrades to empty-looking values for a `"contact:<uid>"` target whose real `Contact` has since vanished
2
+ * (soft-deleted or otherwise) - the same "don't fail the whole ROP over one stale row" principle
3
+ * `MessageTarget.resolveMessageInfo`/`CalendarEventTarget.resolveCalendarEventInfo` already apply. */
4
+ export async function resolveContactInfo(target, contactRepo) {
5
+ const uid = target.slice("contact:".length);
6
+ const contact = await contactRepo.findOne(uid, { ignoreACL: true });
7
+ return {
8
+ displayName: contact?.displayName ?? "",
9
+ givenName: contact?.givenName,
10
+ surname: contact?.surname,
11
+ email: contact?.emails?.[0]?.address,
12
+ businessPhone: contact?.phones?.[0]?.phoneNumber,
13
+ companyName: contact?.company,
14
+ jobTitle: contact?.jobTitle,
15
+ };
16
+ }
17
+ /** The order a `CONTACTS` folder's contents table lists contacts in (see `ContentsTable.ts`). */
18
+ export const CONTACT_SORT = { displayName: "ASC", uid: "ASC" };
19
+ //# sourceMappingURL=ContactTarget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContactTarget.js","sourceRoot":"","sources":["../../../src/rop/ContactTarget.ts"],"names":[],"mappings":"AA8BA;;sGAEsG;AACtG,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,MAAc,EAAE,WAA2B;IAChF,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAwB,MAAM,WAAW,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzF,OAAO;QACH,WAAW,EAAE,OAAO,EAAE,WAAW,IAAI,EAAE;QACvC,SAAS,EAAE,OAAO,EAAE,SAAS;QAC7B,OAAO,EAAE,OAAO,EAAE,OAAO;QACzB,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO;QACpC,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW;QAChD,WAAW,EAAE,OAAO,EAAE,OAAO;QAC7B,QAAQ,EAAE,OAAO,EAAE,QAAQ;KAC9B,CAAC;AACN,CAAC;AAED,iGAAiG;AACjG,MAAM,CAAC,MAAM,YAAY,GAAa,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC"}