@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,241 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import MailComposer from "nodemailer/lib/mail-composer/index.js";
6
+ import { AttendeeResponseStatus, buildEventIcs } from "@rapidmx/restapi";
7
+ import { BufferReader } from "../codec/BufferCursor.js";
8
+ import { isPlainEmailAddress } from "./AddressList.js";
9
+ import { decodeGlobalObjectId, globalObjectIdInstanceDate } from "../codec/GlobalObjectId.js";
10
+ import { LID_GLOBAL_OBJECT_ID, PSETID_MEETING } from "./CalendarNamedProperties.js";
11
+ import { resolveNamedProperty } from "./NamedPropertyRegistry.js";
12
+ import { asEntity, boundIndexedValue, literalQueryValue } from "./RestapiRules.js";
13
+ import { sendOrThrow } from "./TransportSend.js";
14
+ /** `PidTagMessageClass` suffix -> the caller's own new `AttendeeResponseStatus`, per `[MS-OXOCAL]`'s meeting
15
+ * response object naming convention. An unrecognized suffix (a message class this pragmatic subset doesn't
16
+ * know how to interpret as a response) is answered with `MAPI_E_INVALID_PARAMETER`. */
17
+ const RESPONSE_STATUS_BY_MESSAGE_CLASS = {
18
+ "IPM.Schedule.Meeting.Resp.Pos": AttendeeResponseStatus.ACCEPTED,
19
+ "IPM.Schedule.Meeting.Resp.Tent": AttendeeResponseStatus.TENTATIVE,
20
+ "IPM.Schedule.Meeting.Resp.Neg": AttendeeResponseStatus.DECLINED,
21
+ };
22
+ /** Scans `properties` (a draft handle's accumulated `RopSetProperties` values) for the one already-assigned
23
+ * named property matching `(guid, lid)`, returning its raw stored string value - the single-property analog of
24
+ * `RopSaveChangesMessageHandler.ts`'s own full-table decode loop, needed here only for `PidLidGlobalObjectId`. */
25
+ function findNamedPropertyValue(session, properties, guid, lid) {
26
+ for (const [key, value] of Object.entries(properties)) {
27
+ const propertyId = Number(key);
28
+ if (propertyId < 0x8000) {
29
+ continue;
30
+ }
31
+ const named = resolveNamedProperty(session, propertyId);
32
+ if (named && named.kind === "lid" && named.lid === lid && named.guid.toLowerCase() === guid) {
33
+ return value;
34
+ }
35
+ }
36
+ return undefined;
37
+ }
38
+ /** `MAPI_E_INVALID_PARAMETER`: not a response this server understands (unknown message class, no or malformed
39
+ * `PidLidGlobalObjectId`). */
40
+ const ERROR_INVALID_PARAMETER = 0x80070057;
41
+ /** `MAPI_E_NOT_FOUND`: no meeting in the caller's mailbox matches, or the caller isn't one of its attendees. */
42
+ const ERROR_NOT_FOUND = 0x8004010f;
43
+ /** `MAPI_E_CALL_FAILED`: the response was recorded but the iTIP `REPLY` couldn't be sent to the organizer. */
44
+ const ERROR_CALL_FAILED = 0x80004005;
45
+ /**
46
+ * Handles a submitted `"IPM.Schedule.Meeting.Resp.{Pos,Neg,Tent}"` message - an attendee's own response to a
47
+ * meeting invite. Delegated to from `RopSubmitMessageHandler` for that message-class prefix, instead of the ordinary
48
+ * mail or Appointment paths. Returns the ROP's `ReturnValue`.
49
+ *
50
+ * Mirrors restapi's `BaseCalendarEventRoute.respond()`. It correlates the response via `PidLidGlobalObjectId`
51
+ * (echoed by a real client from the invite it's responding to - see `GlobalObjectId.ts`) to **the caller's own
52
+ * copy** of the meeting: the query is scoped to the caller's `mailboxUid`, which is the access check. The
53
+ * organizer's copy (in another mailbox) is never touched here; the organizer's server applies the iTIP `REPLY` sent
54
+ * below, exactly as for a REST response. A native Outlook `GlobalObjectId` decodes to the uppercase hex form of its
55
+ * iCalendar UID, which is matched in either case.
56
+ *
57
+ * **One occurrence of a recurring meeting** (the `GlobalObjectId`'s instance date is set): the caller's exception
58
+ * copy for that date is used when there is one, comparing dates in the event's own time zone. Without one, the
59
+ * series is left alone - responding to one occurrence must not change or delete every other occurrence. Declining
60
+ * adds the occurrence to the caller's series `recurrenceRule.exceptions` (so it leaves their calendar); accepting or
61
+ * tentatively accepting changes nothing locally. Either way the `REPLY` names just that occurrence (`RECURRENCE-ID`).
62
+ *
63
+ * **The whole meeting** (no instance date, or a copy that doesn't recur): the caller's `Attendee.responseStatus` is
64
+ * recorded; declining soft-deletes the caller's copy instead. Declining an occurrence through its exception copy also
65
+ * adds that occurrence to the series' exceptions, so the series doesn't show it again. Every update is versioned
66
+ * (`asEntity`), so a concurrent change fails the response instead of being overwritten.
67
+ *
68
+ * The `REPLY` (restapi's own `buildEventIcs`) goes out through `sendOrThrow`, so a transport that rejects it is
69
+ * reported as `MAPI_E_CALL_FAILED` instead of success; the response itself stays recorded. An organizer or attendee
70
+ * address that isn't a plain SMTP address is not mailed at all. A response that can't be matched to a meeting the
71
+ * caller attends is an error (`MAPI_E_NOT_FOUND`/`MAPI_E_INVALID_PARAMETER`), not a silent success.
72
+ */
73
+ export async function submitMeetingResponse(messageClass, draftProperties, context) {
74
+ const responseStatus = RESPONSE_STATUS_BY_MESSAGE_CLASS[messageClass];
75
+ const globalObjectIdBase64 = responseStatus
76
+ ? findNamedPropertyValue(context.session, draftProperties, PSETID_MEETING, LID_GLOBAL_OBJECT_ID)
77
+ : undefined;
78
+ if (!globalObjectIdBase64) {
79
+ return ERROR_INVALID_PARAMETER;
80
+ }
81
+ const globalObjectId = Buffer.from(globalObjectIdBase64, "base64");
82
+ let icalUid;
83
+ try {
84
+ icalUid = decodeGlobalObjectId(new BufferReader(globalObjectId));
85
+ }
86
+ catch {
87
+ return ERROR_INVALID_PARAMETER;
88
+ }
89
+ const copies = await findCallerCopies(context, icalUid);
90
+ const picked = pickOccurrence(copies, globalObjectIdInstanceDate(globalObjectId));
91
+ const mailbox = picked ? await context.mailboxRepo.findOne(context.mailboxUid, { ignoreACL: true }) : undefined;
92
+ if (!picked || !mailbox) {
93
+ return ERROR_NOT_FOUND;
94
+ }
95
+ const { event, occurrence } = picked;
96
+ const callerAddresses = new Set([mailbox.primarySmtpAddress.toLowerCase(), ...mailbox.aliasAddresses.map((a) => a.toLowerCase())]);
97
+ const respondingAttendee = event.attendees.find((attendee) => callerAddresses.has(attendee.address.toLowerCase()));
98
+ if (!respondingAttendee) {
99
+ return ERROR_NOT_FOUND;
100
+ }
101
+ const updatedAttendee = { ...respondingAttendee, responseStatus };
102
+ let replyEvent = event;
103
+ if (occurrence) {
104
+ if (responseStatus === AttendeeResponseStatus.DECLINED) {
105
+ await addSeriesException(context, event, occurrence);
106
+ }
107
+ const duration = new Date(event.endDate).getTime() - new Date(event.startDate).getTime();
108
+ replyEvent = { ...event, recurrenceRule: undefined, recurrenceId: occurrence, startDate: occurrence, endDate: new Date(occurrence.getTime() + duration) };
109
+ }
110
+ else if (responseStatus === AttendeeResponseStatus.DECLINED) {
111
+ await context.calendarEventRepo.delete(event.uid, { ignoreACL: true });
112
+ // Declining an occurrence that has its own exception copy: deleting the copy alone would bring the series'
113
+ // original occurrence back, so it is excluded from the series too (as restapi's ScanQueueJob does for an
114
+ // occurrence CANCEL).
115
+ const series = copies.find((copy) => copy.recurrenceId == null);
116
+ if (event.recurrenceId != null && series?.recurrenceRule) {
117
+ await addSeriesException(context, series, new Date(event.recurrenceId));
118
+ }
119
+ }
120
+ else {
121
+ const attendees = event.attendees.map((attendee) => (attendee === respondingAttendee ? updatedAttendee : attendee));
122
+ await context.calendarEventRepo.update({ uid: event.uid, version: event.version, attendees }, asEntity(context.calendarEventRepo, event), {
123
+ ignoreACL: true,
124
+ });
125
+ }
126
+ try {
127
+ await sendReply(context, replyEvent, updatedAttendee, mailbox.displayName);
128
+ }
129
+ catch {
130
+ return ERROR_CALL_FAILED;
131
+ }
132
+ return 0;
133
+ }
134
+ /** The most copies of one meeting (series plus exceptions) considered in the caller's mailbox. */
135
+ const MAX_EVENT_COPIES = 100;
136
+ const REPLY_SUBJECT_PREFIX = {
137
+ [AttendeeResponseStatus.ACCEPTED]: "Accepted",
138
+ [AttendeeResponseStatus.TENTATIVE]: "Tentative",
139
+ [AttendeeResponseStatus.DECLINED]: "Declined",
140
+ };
141
+ /** Adds `occurrence` to `series`' recurrence exceptions (a versioned update), unless it is already there. */
142
+ async function addSeriesException(context, series, occurrence) {
143
+ const rule = series.recurrenceRule;
144
+ const exceptions = (rule.exceptions ?? []).map((date) => new Date(date));
145
+ if (exceptions.some((date) => date.getTime() === occurrence.getTime())) {
146
+ return;
147
+ }
148
+ const recurrenceRule = { ...rule, exceptions: [...exceptions, occurrence] };
149
+ await context.calendarEventRepo.update({ uid: series.uid, version: series.version, recurrenceRule }, asEntity(context.calendarEventRepo, series), {
150
+ ignoreACL: true,
151
+ });
152
+ }
153
+ /**
154
+ * The caller's copies of the meeting `icalUid` names. An `OutlookID`'s hex UID may have been stored in either case, so a
155
+ * miss is retried in lower case.
156
+ *
157
+ * The UID comes from the client (and originally from whoever sent the invite), so it is looked up the way restapi
158
+ * stores it (`boundIndexedValue`, hashed past 255 characters), matched literally (a UID like `ne(x)` is not an
159
+ * operator), and every returned row's `icalUid` is compared again.
160
+ */
161
+ async function findCallerCopies(context, icalUid) {
162
+ const find = async (uid) => {
163
+ const key = boundIndexedValue(uid);
164
+ try {
165
+ const rows = await context.calendarEventRepo.find({ icalUid: literalQueryValue(key), mailboxUid: context.mailboxUid, limit: MAX_EVENT_COPIES }, { ignoreACL: true, limit: MAX_EVENT_COPIES });
166
+ return rows.filter((row) => row.icalUid === key);
167
+ }
168
+ catch {
169
+ return []; // a UID the query layer can't take as a value matches nothing
170
+ }
171
+ };
172
+ const events = await find(icalUid);
173
+ return events.length > 0 || icalUid.toLowerCase() === icalUid ? events : find(icalUid.toLowerCase());
174
+ }
175
+ /** `timeZone` when `Intl` accepts it, otherwise `"UTC"`. */
176
+ function usableTimeZone(timeZone) {
177
+ try {
178
+ return new Intl.DateTimeFormat("en-US", { timeZone: timeZone || "UTC" }).resolvedOptions().timeZone;
179
+ }
180
+ catch {
181
+ return "UTC";
182
+ }
183
+ }
184
+ /** `date`'s calendar date (`YYYY-MM-DD`) in `timeZone`. */
185
+ function dateInZone(date, timeZone) {
186
+ return new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).format(date);
187
+ }
188
+ /** How far `timeZone`'s wall clock is ahead of UTC at `date`, in milliseconds (minute precision). */
189
+ function zoneOffsetMs(date, timeZone) {
190
+ const parts = {};
191
+ const format = new Intl.DateTimeFormat("en-US", { timeZone, hourCycle: "h23", year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric" });
192
+ for (const part of format.formatToParts(date)) {
193
+ parts[part.type] = part.value;
194
+ }
195
+ const wallClock = Date.UTC(Number(parts.year), Number(parts.month) - 1, Number(parts.day), Number(parts.hour), Number(parts.minute));
196
+ return wallClock - Math.floor(date.getTime() / 60000) * 60000;
197
+ }
198
+ /** The start of `series`' occurrence on `instanceDate`: the series' local start time on that date in the series'
199
+ * own time zone, so a daylight-saving change in between keeps the same wall-clock time. */
200
+ function occurrenceStart(series, instanceDate) {
201
+ const timeZone = usableTimeZone(series.timezone);
202
+ const start = new Date(series.startDate);
203
+ const days = Math.round((Date.parse(instanceDate) - Date.parse(dateInZone(start, timeZone))) / 86400000);
204
+ const shifted = new Date(start.getTime() + days * 86400000);
205
+ return new Date(shifted.getTime() - (zoneOffsetMs(shifted, timeZone) - zoneOffsetMs(start, timeZone)));
206
+ }
207
+ /**
208
+ * The copy a response applies to. For an `instanceDate`, the caller's exception copy for that date (its
209
+ * `recurrenceId`'s date in the event's time zone) when one exists; otherwise the series (no `recurrenceId`), with
210
+ * `occurrence` set to the named occurrence's start when the series recurs.
211
+ */
212
+ function pickOccurrence(events, instanceDate) {
213
+ const exception = instanceDate
214
+ ? events.find((event) => event.recurrenceId != null && dateInZone(new Date(event.recurrenceId), usableTimeZone(event.timezone)) === instanceDate)
215
+ : undefined;
216
+ const series = events.find((event) => event.recurrenceId == null);
217
+ if (exception || !series) {
218
+ return exception ? { event: exception } : undefined;
219
+ }
220
+ return { event: series, occurrence: instanceDate && series.recurrenceRule ? occurrenceStart(series, instanceDate) : undefined };
221
+ }
222
+ /** Sends the iTIP `REPLY` for `attendee`'s response to the organizer, the same message `BaseCalendarEventRoute.respond()`
223
+ * sends. Throws when the transport doesn't accept it. */
224
+ async function sendReply(context, event, attendee, displayName) {
225
+ const organizerAddress = event.organizer?.address ?? "";
226
+ if (!isPlainEmailAddress(organizerAddress) || !isPlainEmailAddress(attendee.address)) {
227
+ return;
228
+ }
229
+ const ics = buildEventIcs({ ...event, attendees: [attendee] }, "REPLY", { onlyAttendee: attendee });
230
+ const raw = await new MailComposer({
231
+ from: { name: displayName ?? "", address: attendee.address },
232
+ to: organizerAddress,
233
+ subject: `${REPLY_SUBJECT_PREFIX[attendee.responseStatus]}: ${event.title}`,
234
+ text: `${attendee.displayName ?? attendee.address} has responded ${attendee.responseStatus} to: ${event.title}`,
235
+ icalEvent: { method: "reply", content: ics },
236
+ })
237
+ .compile()
238
+ .build();
239
+ await sendOrThrow(context.mailTransport, { raw, envelopeFrom: attendee.address, envelopeTo: [organizerAddress] });
240
+ }
241
+ //# sourceMappingURL=MeetingMessageClassHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MeetingMessageClassHandler.js","sourceRoot":"","sources":["../../../src/rop/MeetingMessageClassHandler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { simpleParser } from "mailparser";
6
+ import { BufferWriter } from "../codec/BufferCursor.js";
7
+ import { handleDataCache, handleDataKey } from "./HandleDataCache.js";
8
+ /** `PidTagBody` (`[MS-OXPROPS]`): property ID `0x1000`, `PtypString`. The only streamable property this
9
+ * pragmatic subset supports via `RopOpenStream`/`RopReadStream` - `PidTagHtml`/`PidTagRtfCompressed` are a
10
+ * documented gap (see `RopOpenStreamHandler`'s own doc comment). */
11
+ export const PID_TAG_BODY = 0x1000;
12
+ /**
13
+ * Resolves a `"message:<uid>"` target's plain-text body as the exact byte sequence `RopReadStream` must serve
14
+ * for a streamed `PtypString` property - confirmed via `[MS-OXCPRPT]`'s `RopOpenStream` page ("a string of
15
+ * Unicode characters in UTF-16LE format encoding with terminating null character"), the identical encoding
16
+ * `writePropertyValue()` already uses for an inline `PtypString` value, so this reuses
17
+ * `BufferWriter.writeNullTerminatedUtf16LE` rather than reimplementing it.
18
+ *
19
+ * Always derived from the message's raw MIME source (`bodyBlobKey`, via `mailparser` - the same fallback path
20
+ * `ItemOperationsCommand.fetchMessage()` already uses for EAS's own plain-text body delivery) rather than
21
+ * `sanitizedHtmlBlobKey` - `PidTagBody` is specifically the plain-text body per `[MS-OXPROPS]`, unlike EAS's
22
+ * `Body` element, which can carry either format tagged by its own `Type` field.
23
+ *
24
+ * Does no caching itself - `loadStreamBody` below is what the stream ROPs call. With a `budget`, nothing is fetched
25
+ * once its bytes are used up, and the raw message's size is charged before it is parsed.
26
+ */
27
+ export async function resolveMessageBodyBytes(target, messageRepo, blobStore, budget) {
28
+ budget?.assertBytesLeft();
29
+ const uid = target.slice("message:".length);
30
+ const message = await messageRepo.findOne(uid, { ignoreACL: true });
31
+ if (!message) {
32
+ return Buffer.alloc(0);
33
+ }
34
+ const raw = await blobStore.get(message.bodyBlobKey);
35
+ budget?.chargeBytes(raw.length);
36
+ const parsed = await simpleParser(raw);
37
+ const writer = new BufferWriter();
38
+ writer.writeNullTerminatedUtf16LE(parsed.text ?? "");
39
+ return writer.toBuffer();
40
+ }
41
+ /**
42
+ * The body bytes for the read stream at `handleIndex`. Parsed once and kept in `HandleDataCache` (outside the
43
+ * session JSON) for the life of the handle, so reading a large body in many `RopReadStream` chunks costs one
44
+ * fetch and one MIME parse instead of one per chunk. A cache miss (eviction, or a request served by another
45
+ * replica) just resolves the body again: a stored message's body never changes, so the bytes are the same.
46
+ *
47
+ * Within one `Execute`, streams opened on the same message share a single parse (`ExecuteBudget.bodies`), and each
48
+ * parse's bytes count against the request's byte budget.
49
+ */
50
+ export async function loadStreamBody(context, handleIndex, stream) {
51
+ const key = handleDataKey(context.session.uid, handleIndex, stream.generation);
52
+ const cached = handleDataCache.get(key);
53
+ if (cached) {
54
+ return cached;
55
+ }
56
+ const budget = context.budget;
57
+ let pending = budget?.bodies.get(stream.entityUid);
58
+ if (!pending) {
59
+ budget?.assertBytesLeft();
60
+ pending = resolveMessageBodyBytes(stream.entityUid, context.messageRepo, context.blobStore, budget);
61
+ budget?.bodies.set(stream.entityUid, pending);
62
+ budget?.chargeBytes((await pending).length);
63
+ }
64
+ const bytes = await pending;
65
+ handleDataCache.set(key, bytes);
66
+ return bytes;
67
+ }
68
+ //# sourceMappingURL=MessageBodyStream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MessageBodyStream.js","sourceRoot":"","sources":["../../../src/rop/MessageBodyStream.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG1C,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAGxD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAGtE;;oEAEoE;AACpE,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC;AAEnC;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,MAAc,EAAE,WAA2B,EAAE,SAAoB,EAAE,MAAsB;IACnI,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,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,IAAI,CAAC,OAAO,EAAE,CAAC;QACX,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC3B,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACrD,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAChC,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;IAClC,MAAM,CAAC,0BAA0B,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IACrD,OAAO,MAAM,CAAC,QAAQ,EAAE,CAAC;AAC7B,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAmB,EAAE,WAAmB,EAAE,MAAwB;IACnG,MAAM,GAAG,GAAG,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IAC/E,MAAM,MAAM,GAAG,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACxC,IAAI,MAAM,EAAE,CAAC;QACT,OAAO,MAAM,CAAC;IAClB,CAAC;IACD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC9B,IAAI,OAAO,GAAG,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACnD,IAAI,CAAC,OAAO,EAAE,CAAC;QACX,MAAM,EAAE,eAAe,EAAE,CAAC;QAC1B,OAAO,GAAG,uBAAuB,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QACpG,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAC9C,MAAM,EAAE,WAAW,CAAC,CAAC,MAAM,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC;IAChD,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC;IAC5B,eAAe,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAChC,OAAO,KAAK,CAAC;AACjB,CAAC"}
@@ -0,0 +1,57 @@
1
+ import { findAllCapped, MAX_COLLECTION_ROWS } from "./RepoPaging.js";
2
+ /** Degrades to empty-looking values for a `"message:<uid>"` target whose real `Message` has since vanished
3
+ * (soft-deleted or otherwise), the same "don't fail the whole ROP over one stale row" principle
4
+ * `FolderTarget.resolveFolderInfo` already applies. */
5
+ export async function resolveMessageInfo(target, messageRepo) {
6
+ const uid = target.slice("message:".length);
7
+ const message = await messageRepo.findOne(uid, { ignoreACL: true });
8
+ return {
9
+ subject: message?.subject ?? "",
10
+ read: message?.flags?.read ?? false,
11
+ hasAttachments: message?.hasAttachments ?? false,
12
+ receivedDate: message?.receivedDate ?? new Date(0),
13
+ labelUids: message?.labelUids ?? [],
14
+ };
15
+ }
16
+ /** The order a folder's messages are listed in: newest first, ties broken by `uid` so paging is stable. */
17
+ export const MESSAGE_SORT = { receivedDate: "DESC", uid: "ASC" };
18
+ /** Resolves the messages directly in `folderUid`, newest first and capped at `MAX_COLLECTION_ROWS`, as
19
+ * `"message:<uid>"` target strings. */
20
+ export async function resolveFolderMessages(folderUid, messageRepo, budget) {
21
+ const { items: messages } = await findAllCapped(messageRepo, { folderUid }, MESSAGE_SORT, MAX_COLLECTION_ROWS, budget);
22
+ return messages.map((m) => `message:${m.uid}`);
23
+ }
24
+ /**
25
+ * Returns `target`'s existing MID if an earlier `RopQueryRows` row already assigned one, otherwise assigns and
26
+ * remembers the next free small integer MID. The exact `FolderTarget.assignOrGetFid` pattern, adapted for
27
+ * messages: this is what lets a client `RopOpenMessage` a message it only ever learned about via a
28
+ * `RopQueryRows` row's `PidTagMid` column.
29
+ *
30
+ * Backed by `session.messageTargetIds` (target -> MID) and `session.nextMessageId`, an O(1) reverse index/
31
+ * counter pair rather than a linear scan of `session.messageIds` plus a `Math.max(...spread)` over its keys -
32
+ * see `FolderTarget.assignOrGetFid`'s own doc comment for why both of those were real costs (not just
33
+ * theoretical ones) at real mailbox/session scale: `messageIds` only ever grows for a session's lifetime as a
34
+ * client pages through a mailbox, so a linear-scan lookup repeated once per row is quadratic over a session
35
+ * that pages through many messages.
36
+ */
37
+ export function assignOrGetMid(session, target) {
38
+ const existing = session.messageTargetIds[target];
39
+ if (existing !== undefined) {
40
+ return existing;
41
+ }
42
+ const mid = session.nextMessageId++;
43
+ session.messageIds[String(mid)] = target;
44
+ session.messageTargetIds[target] = mid;
45
+ // MIDs are handed out consecutively, so the mapped ones are exactly [firstMessageId, nextMessageId). Past the cap
46
+ // the oldest is forgotten: opening it by that MID fails until a table row assigns the item a new one.
47
+ while (session.nextMessageId - session.firstMessageId > MAX_MESSAGE_IDS) {
48
+ const oldest = String(session.firstMessageId++);
49
+ delete session.messageTargetIds[session.messageIds[oldest]];
50
+ delete session.messageIds[oldest];
51
+ }
52
+ return mid;
53
+ }
54
+ /** The most MIDs one session keeps mapped. A session paging through a very large mailbox would otherwise grow its
55
+ * MID maps - saved with the session on every `Execute` - without bound. */
56
+ export const MAX_MESSAGE_IDS = 20000;
57
+ //# sourceMappingURL=MessageTarget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MessageTarget.js","sourceRoot":"","sources":["../../../src/rop/MessageTarget.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAiB,MAAM,iBAAiB,CAAC;AAkBpF;;uDAEuD;AACvD,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,OAAO,EAAE,OAAO,EAAE,OAAO,IAAI,EAAE;QAC/B,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,IAAI,KAAK;QACnC,cAAc,EAAE,OAAO,EAAE,cAAc,IAAI,KAAK;QAChD,YAAY,EAAE,OAAO,EAAE,YAAY,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC;QAClD,SAAS,EAAE,OAAO,EAAE,SAAS,IAAI,EAAE;KACtC,CAAC;AACN,CAAC;AAED,2GAA2G;AAC3G,MAAM,CAAC,MAAM,YAAY,GAAa,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;AAE3E;uCACuC;AACvC,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,SAAiB,EAAE,WAA2B,EAAE,MAAsB;IAC9G,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,MAAM,aAAa,CAAU,WAAW,EAAE,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,CAAC,CAAC;IAChI,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc,CAAC,OAA2B,EAAE,MAAc;IACtE,MAAM,QAAQ,GAAG,OAAO,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAClD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,QAAQ,CAAC;IACpB,CAAC;IACD,MAAM,GAAG,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IACpC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC;IACzC,OAAO,CAAC,gBAAgB,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC;IACvC,kHAAkH;IAClH,sGAAsG;IACtG,OAAO,OAAO,CAAC,aAAa,GAAG,OAAO,CAAC,cAAc,GAAG,eAAe,EAAE,CAAC;QACtE,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;QAChD,OAAO,OAAO,CAAC,gBAAgB,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;QAC5D,OAAO,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC;AAED;2EAC2E;AAC3E,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,CAAC"}
@@ -0,0 +1,69 @@
1
+ /** The first numeric property ID this pragmatic subset ever assigns to a named property - real Exchange also
2
+ * reserves the `0x0000`-`0x7FFF` range for well-known/`PidTag*` properties, only ever assigning named
3
+ * properties IDs at `0x8000` and above (`[MS-OXCPRPT]`'s own `RopGetPropertyIdsFromNames` processing rules). */
4
+ const FIRST_NAMED_PROPERTY_ID = 0x8000;
5
+ /** A `PropertyName`'s registry key - a JSON string rather than a delimiter-joined one (e.g. `"<guid>:<lid>"`)
6
+ * because a string-`Kind` named property's own `name` is an arbitrary client-supplied string that could
7
+ * legitimately contain any delimiter this codec might otherwise pick (some real named properties do use
8
+ * URN-shaped names). `JSON.stringify` on a small, fixed-shape object has no such ambiguity. */
9
+ function keyFor(propertyName) {
10
+ return propertyName.kind === "lid"
11
+ ? JSON.stringify({ guid: propertyName.guid.toLowerCase(), kind: "lid", lid: propertyName.lid })
12
+ : JSON.stringify({ guid: propertyName.guid.toLowerCase(), kind: "name", name: propertyName.name });
13
+ }
14
+ /** The most distinct named properties one session registers. The registry is saved with the session on every
15
+ * `Execute`, and a name can be up to 255 bytes, so letting a client fill the whole `0x8000`-`0xFFFF` ID space
16
+ * would make every request carry megabytes of session state. Real clients register a few hundred at most. */
17
+ export const MAX_NAMED_PROPERTIES_PER_SESSION = 4096;
18
+ /** The highest numeric property ID handed out - see `MAX_NAMED_PROPERTIES_PER_SESSION`. */
19
+ const LAST_NAMED_PROPERTY_ID = FIRST_NAMED_PROPERTY_ID + MAX_NAMED_PROPERTIES_PER_SESSION - 1;
20
+ /**
21
+ * Returns `propertyName`'s existing numeric property ID if an earlier `RopGetPropertyIdsFromNames` call in
22
+ * this session already assigned one, otherwise assigns and remembers the next free ID starting at
23
+ * `FIRST_NAMED_PROPERTY_ID`. This pragmatic subset always behaves as if the request's own `Flags` field
24
+ * requested "assign a new ID if unmapped" (`0x02`) - real Exchange's alternative (`0x00`, "only return
25
+ * already-mapped IDs") exists to let a client probe without committing a mailbox-wide registration; since this
26
+ * registry is already only ever session-scoped (not a real persisted per-mailbox mapping table), there is no
27
+ * meaningful difference between "probe" and "assign" here.
28
+ *
29
+ * Backed by `session.namedProperties`/`namedPropertyIds` (a forward and reverse map, kept in sync) and
30
+ * `session.nextNamedPropertyId`, an O(1) lookup/assignment pair rather than a linear scan plus a
31
+ * `Math.max(...spread)` over `Object.values(session.namedProperties)` - both real costs at scale: the spread
32
+ * form risks a `RangeError: Maximum call stack size exceeded` once a session has registered enough distinct
33
+ * names to exceed V8's function-argument-count limit, and the registry only ever grows for a session's
34
+ * lifetime, so a linear scan repeated once per property per row is quadratic over a session that resolves many
35
+ * named properties.
36
+ *
37
+ * Once `nextNamedPropertyId` would exceed `LAST_NAMED_PROPERTY_ID` (`MAX_NAMED_PROPERTIES_PER_SESSION` distinct
38
+ * names already registered this session), a *new*
39
+ * name can no longer be assigned a real ID; this returns `0x0000` for it instead of throwing, the exact value
40
+ * `[MS-OXCPRPT]`'s own `RopGetPropertyIdsFromNames` processing rules already use for "this `PropertyName`
41
+ * could not be resolved" (the same value this pragmatic subset already produces for a `Kind = 0xFF` entry) -
42
+ * not a new failure mode, just the same one applied to a different unmappable case. A name already registered
43
+ * before the registry filled up keeps returning its real, previously-assigned ID.
44
+ */
45
+ export function assignOrGetNamedPropertyId(session, propertyName) {
46
+ const key = keyFor(propertyName);
47
+ const existing = session.namedProperties[key];
48
+ if (existing !== undefined) {
49
+ return existing;
50
+ }
51
+ if (session.nextNamedPropertyId > LAST_NAMED_PROPERTY_ID) {
52
+ return 0x0000;
53
+ }
54
+ const id = session.nextNamedPropertyId++;
55
+ session.namedProperties[key] = id;
56
+ session.namedPropertyIds[id] = key;
57
+ return id;
58
+ }
59
+ /** The reverse lookup `RopSetProperties`/`RopGetPropertiesSpecific`/`RopQueryRows` use to recognize an incoming
60
+ * property ID `>= 0x8000` as one of this session's own mapped named properties - an O(1) lookup against
61
+ * `session.namedPropertyIds` (kept in sync by `assignOrGetNamedPropertyId`) rather than a linear scan of
62
+ * `session.namedProperties` with a `JSON.parse` per candidate - see that function's own doc comment for why
63
+ * this matters at scale, particularly since this is called once per named-property column per row from
64
+ * `RopQueryRows`. */
65
+ export function resolveNamedProperty(session, propertyId) {
66
+ const key = session.namedPropertyIds[propertyId];
67
+ return key !== undefined ? JSON.parse(key) : undefined;
68
+ }
69
+ //# sourceMappingURL=NamedPropertyRegistry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NamedPropertyRegistry.js","sourceRoot":"","sources":["../../../src/rop/NamedPropertyRegistry.ts"],"names":[],"mappings":"AAmBA;;gHAEgH;AAChH,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAEvC;;;+FAG+F;AAC/F,SAAS,MAAM,CAAC,YAA0B;IACtC,OAAO,YAAY,CAAC,IAAI,KAAK,KAAK;QAC9B,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,YAAY,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,YAAY,CAAC,GAAG,EAAE,CAAC;QAC/F,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,YAAY,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC;AAC3G,CAAC;AAED;;6GAE6G;AAC7G,MAAM,CAAC,MAAM,gCAAgC,GAAG,IAAI,CAAC;AAErD,2FAA2F;AAC3F,MAAM,sBAAsB,GAAG,uBAAuB,GAAG,gCAAgC,GAAG,CAAC,CAAC;AAE9F;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,0BAA0B,CAAC,OAA2B,EAAE,YAA0B;IAC9F,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;IACjC,MAAM,QAAQ,GAAG,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;IAC9C,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,QAAQ,CAAC;IACpB,CAAC;IACD,IAAI,OAAO,CAAC,mBAAmB,GAAG,sBAAsB,EAAE,CAAC;QACvD,OAAO,MAAM,CAAC;IAClB,CAAC;IACD,MAAM,EAAE,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC;IACzC,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;IAClC,OAAO,CAAC,gBAAgB,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC;IACnC,OAAO,EAAE,CAAC;AACd,CAAC;AAED;;;;;qBAKqB;AACrB,MAAM,UAAU,oBAAoB,CAAC,OAA2B,EAAE,UAAkB;IAChF,MAAM,GAAG,GAAG,OAAO,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC;IACjD,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAkB,CAAC,CAAC,CAAC,SAAS,CAAC;AAC7E,CAAC"}