@rapidmx/mapi-plugin 1.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/LICENSE +382 -0
  2. package/README.md +60 -0
  3. package/dist/lib/BaseMapiEmsmdbRoute.js +297 -0
  4. package/dist/lib/BaseMapiEmsmdbRoute.js.map +1 -0
  5. package/dist/lib/BaseMapiNspiRoute.js +114 -0
  6. package/dist/lib/BaseMapiNspiRoute.js.map +1 -0
  7. package/dist/lib/MapiSessionManager.js +119 -0
  8. package/dist/lib/MapiSessionManager.js.map +1 -0
  9. package/dist/lib/RopDispatcher.js +34 -0
  10. package/dist/lib/RopDispatcher.js.map +1 -0
  11. package/dist/lib/codec/AppointmentRecurrence.js +314 -0
  12. package/dist/lib/codec/AppointmentRecurrence.js.map +1 -0
  13. package/dist/lib/codec/BufferCursor.js +213 -0
  14. package/dist/lib/codec/BufferCursor.js.map +1 -0
  15. package/dist/lib/codec/GlobalObjectId.js +92 -0
  16. package/dist/lib/codec/GlobalObjectId.js.map +1 -0
  17. package/dist/lib/codec/MapiGuid.js +41 -0
  18. package/dist/lib/codec/MapiGuid.js.map +1 -0
  19. package/dist/lib/codec/MapiTimeZone.js +93 -0
  20. package/dist/lib/codec/MapiTimeZone.js.map +1 -0
  21. package/dist/lib/codec/PropertyValue.js +214 -0
  22. package/dist/lib/codec/PropertyValue.js.map +1 -0
  23. package/dist/lib/codec/RopBuffer.js +26 -0
  24. package/dist/lib/codec/RopBuffer.js.map +1 -0
  25. package/dist/lib/codec/TypedString.js +54 -0
  26. package/dist/lib/codec/TypedString.js.map +1 -0
  27. package/dist/lib/index.js +99 -0
  28. package/dist/lib/index.js.map +1 -0
  29. package/dist/lib/mongo/MapiEmsmdbRouteMongo.js +84 -0
  30. package/dist/lib/mongo/MapiEmsmdbRouteMongo.js.map +1 -0
  31. package/dist/lib/mongo/MapiNspiRouteMongo.js +33 -0
  32. package/dist/lib/mongo/MapiNspiRouteMongo.js.map +1 -0
  33. package/dist/lib/mongo/index.js +7 -0
  34. package/dist/lib/mongo/index.js.map +1 -0
  35. package/dist/lib/mongo.js +6 -0
  36. package/dist/lib/mongo.js.map +1 -0
  37. package/dist/lib/nspi/NspiBindHandler.js +54 -0
  38. package/dist/lib/nspi/NspiBindHandler.js.map +1 -0
  39. package/dist/lib/nspi/NspiCodec.js +106 -0
  40. package/dist/lib/nspi/NspiCodec.js.map +1 -0
  41. package/dist/lib/nspi/NspiGetMatchesHandler.js +142 -0
  42. package/dist/lib/nspi/NspiGetMatchesHandler.js.map +1 -0
  43. package/dist/lib/rop/CalendarEventTarget.js +27 -0
  44. package/dist/lib/rop/CalendarEventTarget.js.map +1 -0
  45. package/dist/lib/rop/CalendarNamedProperties.js +57 -0
  46. package/dist/lib/rop/CalendarNamedProperties.js.map +1 -0
  47. package/dist/lib/rop/ContactTarget.js +23 -0
  48. package/dist/lib/rop/ContactTarget.js.map +1 -0
  49. package/dist/lib/rop/FastTransferStream.js +98 -0
  50. package/dist/lib/rop/FastTransferStream.js.map +1 -0
  51. package/dist/lib/rop/FolderTarget.js +111 -0
  52. package/dist/lib/rop/FolderTarget.js.map +1 -0
  53. package/dist/lib/rop/MeetingMessageClassHandler.js +95 -0
  54. package/dist/lib/rop/MeetingMessageClassHandler.js.map +1 -0
  55. package/dist/lib/rop/MessageBodyStream.js +42 -0
  56. package/dist/lib/rop/MessageBodyStream.js.map +1 -0
  57. package/dist/lib/rop/MessageTarget.js +44 -0
  58. package/dist/lib/rop/MessageTarget.js.map +1 -0
  59. package/dist/lib/rop/NamedPropertyRegistry.js +66 -0
  60. package/dist/lib/rop/NamedPropertyRegistry.js.map +1 -0
  61. package/dist/lib/rop/PropertyResolvers.js +313 -0
  62. package/dist/lib/rop/PropertyResolvers.js.map +1 -0
  63. package/dist/lib/rop/RopCreateMessageHandler.js +53 -0
  64. package/dist/lib/rop/RopCreateMessageHandler.js.map +1 -0
  65. package/dist/lib/rop/RopDeleteFolderHandler.js +133 -0
  66. package/dist/lib/rop/RopDeleteFolderHandler.js.map +1 -0
  67. package/dist/lib/rop/RopDeleteMessagesHandler.js +70 -0
  68. package/dist/lib/rop/RopDeleteMessagesHandler.js.map +1 -0
  69. package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js +61 -0
  70. package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js.map +1 -0
  71. package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js +61 -0
  72. package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js.map +1 -0
  73. package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js +75 -0
  74. package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js.map +1 -0
  75. package/dist/lib/rop/RopGetContentsTableHandler.js +76 -0
  76. package/dist/lib/rop/RopGetContentsTableHandler.js.map +1 -0
  77. package/dist/lib/rop/RopGetHierarchyTableHandler.js +40 -0
  78. package/dist/lib/rop/RopGetHierarchyTableHandler.js.map +1 -0
  79. package/dist/lib/rop/RopGetPropertiesSpecificHandler.js +55 -0
  80. package/dist/lib/rop/RopGetPropertiesSpecificHandler.js.map +1 -0
  81. package/dist/lib/rop/RopGetPropertyIdsFromNamesHandler.js +92 -0
  82. package/dist/lib/rop/RopGetPropertyIdsFromNamesHandler.js.map +1 -0
  83. package/dist/lib/rop/RopHandler.js +2 -0
  84. package/dist/lib/rop/RopHandler.js.map +1 -0
  85. package/dist/lib/rop/RopLogonHandler.js +132 -0
  86. package/dist/lib/rop/RopLogonHandler.js.map +1 -0
  87. package/dist/lib/rop/RopOpenFolderHandler.js +46 -0
  88. package/dist/lib/rop/RopOpenFolderHandler.js.map +1 -0
  89. package/dist/lib/rop/RopOpenMessageHandler.js +81 -0
  90. package/dist/lib/rop/RopOpenMessageHandler.js.map +1 -0
  91. package/dist/lib/rop/RopOpenStreamHandler.js +81 -0
  92. package/dist/lib/rop/RopOpenStreamHandler.js.map +1 -0
  93. package/dist/lib/rop/RopQueryRowsHandler.js +91 -0
  94. package/dist/lib/rop/RopQueryRowsHandler.js.map +1 -0
  95. package/dist/lib/rop/RopReadStreamHandler.js +55 -0
  96. package/dist/lib/rop/RopReadStreamHandler.js.map +1 -0
  97. package/dist/lib/rop/RopReleaseHandler.js +20 -0
  98. package/dist/lib/rop/RopReleaseHandler.js.map +1 -0
  99. package/dist/lib/rop/RopSaveChangesMessageHandler.js +210 -0
  100. package/dist/lib/rop/RopSaveChangesMessageHandler.js.map +1 -0
  101. package/dist/lib/rop/RopSetColumnsHandler.js +52 -0
  102. package/dist/lib/rop/RopSetColumnsHandler.js.map +1 -0
  103. package/dist/lib/rop/RopSetPropertiesHandler.js +162 -0
  104. package/dist/lib/rop/RopSetPropertiesHandler.js.map +1 -0
  105. package/dist/lib/rop/RopSubmitMessageHandler.js +314 -0
  106. package/dist/lib/rop/RopSubmitMessageHandler.js.map +1 -0
  107. package/dist/lib/rop/RopWriteStreamHandler.js +45 -0
  108. package/dist/lib/rop/RopWriteStreamHandler.js.map +1 -0
  109. package/dist/lib/rop/TaskNamedProperties.js +29 -0
  110. package/dist/lib/rop/TaskNamedProperties.js.map +1 -0
  111. package/dist/lib/rop/TaskTarget.js +19 -0
  112. package/dist/lib/rop/TaskTarget.js.map +1 -0
  113. package/dist/lib/sql/MapiEmsmdbRouteSQL.js +84 -0
  114. package/dist/lib/sql/MapiEmsmdbRouteSQL.js.map +1 -0
  115. package/dist/lib/sql/MapiNspiRouteSQL.js +33 -0
  116. package/dist/lib/sql/MapiNspiRouteSQL.js.map +1 -0
  117. package/dist/lib/sql/index.js +7 -0
  118. package/dist/lib/sql/index.js.map +1 -0
  119. package/dist/lib/sql.js +6 -0
  120. package/dist/lib/sql.js.map +1 -0
  121. package/dist/types/BaseMapiEmsmdbRoute.d.ts +82 -0
  122. package/dist/types/BaseMapiNspiRoute.d.ts +38 -0
  123. package/dist/types/MapiSessionManager.d.ts +130 -0
  124. package/dist/types/RopDispatcher.d.ts +17 -0
  125. package/dist/types/codec/AppointmentRecurrence.d.ts +14 -0
  126. package/dist/types/codec/BufferCursor.d.ts +78 -0
  127. package/dist/types/codec/GlobalObjectId.d.ts +11 -0
  128. package/dist/types/codec/MapiGuid.d.ts +6 -0
  129. package/dist/types/codec/MapiTimeZone.d.ts +7 -0
  130. package/dist/types/codec/PropertyValue.d.ts +77 -0
  131. package/dist/types/codec/RopBuffer.d.ts +25 -0
  132. package/dist/types/codec/TypedString.d.ts +17 -0
  133. package/dist/types/index.d.ts +94 -0
  134. package/dist/types/mongo/MapiEmsmdbRouteMongo.d.ts +19 -0
  135. package/dist/types/mongo/MapiNspiRouteMongo.d.ts +13 -0
  136. package/dist/types/mongo/index.d.ts +2 -0
  137. package/dist/types/mongo.d.ts +1 -0
  138. package/dist/types/nspi/NspiBindHandler.d.ts +22 -0
  139. package/dist/types/nspi/NspiCodec.d.ts +59 -0
  140. package/dist/types/nspi/NspiGetMatchesHandler.d.ts +24 -0
  141. package/dist/types/rop/CalendarEventTarget.d.ts +29 -0
  142. package/dist/types/rop/CalendarNamedProperties.d.ts +37 -0
  143. package/dist/types/rop/ContactTarget.d.ts +29 -0
  144. package/dist/types/rop/FastTransferStream.d.ts +18 -0
  145. package/dist/types/rop/FolderTarget.d.ts +63 -0
  146. package/dist/types/rop/MeetingMessageClassHandler.d.ts +36 -0
  147. package/dist/types/rop/MessageBodyStream.d.ts +26 -0
  148. package/dist/types/rop/MessageTarget.d.ts +38 -0
  149. package/dist/types/rop/NamedPropertyRegistry.d.ts +46 -0
  150. package/dist/types/rop/PropertyResolvers.d.ts +60 -0
  151. package/dist/types/rop/RopCreateMessageHandler.d.ts +23 -0
  152. package/dist/types/rop/RopDeleteFolderHandler.d.ts +38 -0
  153. package/dist/types/rop/RopDeleteMessagesHandler.d.ts +31 -0
  154. package/dist/types/rop/RopFastTransferSourceCopyPropertiesHandler.d.ts +24 -0
  155. package/dist/types/rop/RopFastTransferSourceCopyToHandler.d.ts +24 -0
  156. package/dist/types/rop/RopFastTransferSourceGetBufferHandler.d.ts +23 -0
  157. package/dist/types/rop/RopGetContentsTableHandler.d.ts +31 -0
  158. package/dist/types/rop/RopGetHierarchyTableHandler.d.ts +17 -0
  159. package/dist/types/rop/RopGetPropertiesSpecificHandler.d.ts +23 -0
  160. package/dist/types/rop/RopGetPropertyIdsFromNamesHandler.d.ts +23 -0
  161. package/dist/types/rop/RopHandler.d.ts +67 -0
  162. package/dist/types/rop/RopLogonHandler.d.ts +31 -0
  163. package/dist/types/rop/RopOpenFolderHandler.d.ts +19 -0
  164. package/dist/types/rop/RopOpenMessageHandler.d.ts +27 -0
  165. package/dist/types/rop/RopOpenStreamHandler.d.ts +27 -0
  166. package/dist/types/rop/RopQueryRowsHandler.d.ts +36 -0
  167. package/dist/types/rop/RopReadStreamHandler.d.ts +18 -0
  168. package/dist/types/rop/RopReleaseHandler.d.ts +14 -0
  169. package/dist/types/rop/RopSaveChangesMessageHandler.d.ts +44 -0
  170. package/dist/types/rop/RopSetColumnsHandler.d.ts +21 -0
  171. package/dist/types/rop/RopSetPropertiesHandler.d.ts +38 -0
  172. package/dist/types/rop/RopSubmitMessageHandler.d.ts +70 -0
  173. package/dist/types/rop/RopWriteStreamHandler.d.ts +21 -0
  174. package/dist/types/rop/TaskNamedProperties.d.ts +24 -0
  175. package/dist/types/rop/TaskTarget.d.ts +24 -0
  176. package/dist/types/sql/MapiEmsmdbRouteSQL.d.ts +19 -0
  177. package/dist/types/sql/MapiNspiRouteSQL.d.ts +13 -0
  178. package/dist/types/sql/index.d.ts +2 -0
  179. package/dist/types/sql.d.ts +1 -0
  180. package/package.json +104 -0
@@ -0,0 +1,210 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import crypto from "crypto";
6
+ import { AttendeeResponseStatus, AttendeeRole, BusyStatus, RecipientType, } from "@rapidmx/restapi";
7
+ import { decodeAppointmentRecurrence } from "../codec/AppointmentRecurrence.js";
8
+ import { BufferReader } from "../codec/BufferCursor.js";
9
+ import { decodeTimeZoneStruct } from "../codec/MapiTimeZone.js";
10
+ import { BUSY_STATUS_FROM_CODE, LID_APPOINTMENT_END_WHOLE, LID_APPOINTMENT_RECUR, LID_APPOINTMENT_START_WHOLE, LID_BUSY_STATUS, LID_LOCATION, LID_REMINDER_DELTA, LID_TIME_ZONE_STRUCT, PSETID_APPOINTMENT, PSETID_COMMON, } from "./CalendarNamedProperties.js";
11
+ import { assignOrGetMid } from "./MessageTarget.js";
12
+ import { resolveNamedProperty } from "./NamedPropertyRegistry.js";
13
+ import { parseAddressList } from "./RopSubmitMessageHandler.js";
14
+ const ROP_ID_SAVE_CHANGES_MESSAGE = 0x0c;
15
+ /** The well-known MAPI HRESULT `MAPI_E_INVALID_OBJECT`, reused for "the referenced handle isn't a message (or
16
+ * doesn't exist)" - the same constant `RopGetPropertiesSpecificHandler` uses for its own analogous check. */
17
+ const ERROR_INVALID_OBJECT = 0x80070005;
18
+ // The same well-known property IDs RopSetPropertiesHandler tracks - duplicated here (rather than imported)
19
+ // since importing them would only save a handful of literals; see that file for the real documentation of what
20
+ // each means.
21
+ const PID_TAG_SUBJECT = 0x0037;
22
+ const PID_TAG_MESSAGE_CLASS = 0x001a;
23
+ const PID_TAG_DISPLAY_CC = 0x0e03;
24
+ const PID_TAG_DISPLAY_TO = 0x0e04;
25
+ /** `PidTagMessageClass` values starting with this prefix route through `saveAppointment()` instead of the
26
+ * generic draft-MID-assignment path below - see that method's own doc comment. */
27
+ const MESSAGE_CLASS_APPOINTMENT_PREFIX = "IPM.Appointment";
28
+ /**
29
+ * `RopSaveChangesMessage` (`[MS-OXCMSG]`/`[MS-OXCROPS]`): persists a message's pending changes, returning the
30
+ * `MID` a client can reference it by afterwards. **Pragmatic subset**: this library has no separate Drafts-
31
+ * folder persistence step (matching EAS's own `ComposeMailCommand`, which never creates a standalone draft
32
+ * `Message` row either) - saving a `RopCreateMessage` draft does not yet write anything to a database. It only
33
+ * assigns a session-scoped MID (`MessageTarget.assignOrGetMid`, the exact mechanism a `RopQueryRows` row's
34
+ * `PidTagMid` column already uses) so the wire contract's `MessageId` field is well-formed, keyed by this
35
+ * handle's own index (`` `draft:${inputHandleIndex}` ``) since an unsaved draft has no real `"message:<uid>"`
36
+ * target yet. The real work - actually building and sending a MIME message from the properties/body
37
+ * accumulated so far - happens at `RopSubmitMessage` time, not here; calling Save without ever calling Submit
38
+ * silently discards the draft when the session expires, a real, deliberate limitation (this pragmatic subset
39
+ * has no autosave-recovery story) rather than a silently-wrong one.
40
+ *
41
+ * Re-saving an already-real message (a `RopOpenMessage` handle, not a fresh `RopCreateMessage` draft) is
42
+ * accepted but is a no-op beyond echoing that message's own already-assigned MID - editing an existing
43
+ * message's properties is out of scope for this pragmatic subset's compose/send-only ROP coverage.
44
+ *
45
+ * `SaveFlags` is decoded to advance past it correctly but not honored - this pragmatic subset has no
46
+ * conflict-resolution/force-save semantics to vary by flag.
47
+ *
48
+ * **Calendar branch**: unlike mail, a draft whose `PidTagMessageClass` starts with `"IPM.Appointment"` *is*
49
+ * durably persisted here (`saveAppointment()`) - creating (or updating, if this handle already refers to a real
50
+ * `"calendarEvent:<uid>"`) a real `CalendarEvent` row, so a subsequent `RopGetPropertiesSpecific` can read it
51
+ * back before `RopSubmitMessage` is ever called (a calendar item must exist as soon as it's saved, the same way
52
+ * a real Exchange server behaves - unlike a mail draft, which this pragmatic subset never persists at all until
53
+ * Submit).
54
+ *
55
+ * @author Jean-Philippe Steinmetz
56
+ */
57
+ export class RopSaveChangesMessageHandler {
58
+ constructor() {
59
+ this.ropId = ROP_ID_SAVE_CHANGES_MESSAGE;
60
+ }
61
+ async handle(reader, writer, context) {
62
+ reader.readUInt8(); // LogonId - this pragmatic subset doesn't track multiple concurrent logons per session
63
+ const responseHandleIndex = reader.readUInt8();
64
+ const inputHandleIndex = reader.readUInt8();
65
+ reader.readUInt8(); // SaveFlags - see class doc comment
66
+ const handle = context.session.handles[inputHandleIndex];
67
+ if (!handle || handle.type !== "message") {
68
+ writer.writeUInt8(ROP_ID_SAVE_CHANGES_MESSAGE);
69
+ writer.writeUInt8(responseHandleIndex);
70
+ writer.writeUInt32LE(ERROR_INVALID_OBJECT);
71
+ return;
72
+ }
73
+ const draftProperties = handle.draftProperties ?? {};
74
+ const messageClass = draftProperties[String(PID_TAG_MESSAGE_CLASS)] ?? "IPM.Note";
75
+ const mid = messageClass.startsWith(MESSAGE_CLASS_APPOINTMENT_PREFIX)
76
+ ? await this.saveAppointment(handle, draftProperties, context)
77
+ : BigInt(assignOrGetMid(context.session, handle.entityUid !== "" ? handle.entityUid : `draft:${inputHandleIndex}`));
78
+ writer.writeUInt8(ROP_ID_SAVE_CHANGES_MESSAGE);
79
+ writer.writeUInt8(responseHandleIndex);
80
+ writer.writeUInt32LE(0); // ReturnValue - success
81
+ writer.writeUInt8(inputHandleIndex);
82
+ writer.writeBigUInt64LE(mid);
83
+ }
84
+ /**
85
+ * Persists (creating, or updating an already-real one) a `CalendarEvent` from the draft's accumulated
86
+ * `RopSetProperties` values, and returns the MID a client can reference it by. Every field this pragmatic
87
+ * subset understands (per `CalendarNamedProperties.ts`) is decoded via `decodeCalendarFieldsFromDraft`
88
+ * below - the write-side mirror of `PropertyResolvers.calendarEventValueFor`'s read-side switch. A field the
89
+ * client never set (e.g. no `RopSetProperties` call touched `PidLidBusyStatus`) falls back to the existing
90
+ * row's own value on an update, or the entity class's own default on a fresh create.
91
+ */
92
+ async saveAppointment(handle, draftProperties, context) {
93
+ const decoded = decodeCalendarFieldsFromDraft(context.session, draftProperties);
94
+ const attendees = decoded.attendeeAddresses.map((address) => ({
95
+ address,
96
+ role: AttendeeRole.REQUIRED,
97
+ responseStatus: AttendeeResponseStatus.NEEDS_ACTION,
98
+ isOrganizer: false,
99
+ }));
100
+ if (handle.entityUid.startsWith("calendarEvent:")) {
101
+ const uid = handle.entityUid.slice("calendarEvent:".length);
102
+ const existing = await context.calendarEventRepo.findOne(uid, {
103
+ ignoreACL: true,
104
+ });
105
+ if (existing) {
106
+ await context.calendarEventRepo.update({
107
+ uid: existing.uid,
108
+ version: existing.version,
109
+ title: decoded.title ?? existing.title,
110
+ location: decoded.location ?? existing.location,
111
+ startDate: decoded.startDate ?? existing.startDate,
112
+ endDate: decoded.endDate ?? existing.endDate,
113
+ busyStatus: decoded.busyStatus ?? existing.busyStatus,
114
+ recurrenceRule: decoded.recurrenceRule ?? existing.recurrenceRule,
115
+ timezone: decoded.timezone ?? existing.timezone,
116
+ reminderMinutesBeforeStart: decoded.reminderMinutesBeforeStart ?? existing.reminderMinutesBeforeStart,
117
+ attendees: attendees.length > 0 ? attendees : existing.attendees,
118
+ }, existing, { ignoreACL: true });
119
+ }
120
+ return BigInt(assignOrGetMid(context.session, handle.entityUid));
121
+ }
122
+ const mailbox = await context.mailboxRepo.findOne(context.mailboxUid, { ignoreACL: true });
123
+ // draftFolderUid is a "folder:<uid>"-prefixed target string (RopCreateMessageHandler stores the same
124
+ // target FolderTarget.assignOrGetFid assigned), not a bare uid - strip the prefix before persisting.
125
+ const draftFolderTarget = handle.draftFolderUid ?? "";
126
+ const folderUid = draftFolderTarget.startsWith("folder:") ? draftFolderTarget.slice("folder:".length) : draftFolderTarget;
127
+ const created = await context.calendarEventRepo.create(new context.calendarEventClass({
128
+ folderUid,
129
+ mailboxUid: context.mailboxUid,
130
+ title: decoded.title ?? "",
131
+ location: decoded.location,
132
+ startDate: decoded.startDate ?? new Date(),
133
+ endDate: decoded.endDate ?? new Date(),
134
+ timezone: decoded.timezone ?? "UTC",
135
+ organizer: { address: mailbox?.primarySmtpAddress ?? "", displayName: mailbox?.displayName, type: RecipientType.TO },
136
+ attendees,
137
+ busyStatus: decoded.busyStatus ?? BusyStatus.BUSY,
138
+ recurrenceRule: decoded.recurrenceRule,
139
+ reminderMinutesBeforeStart: decoded.reminderMinutesBeforeStart,
140
+ icalUid: `${crypto.randomUUID()}@mapi`,
141
+ sequence: 0,
142
+ }), { ignoreACL: true });
143
+ handle.entityUid = `calendarEvent:${created.uid}`;
144
+ return BigInt(assignOrGetMid(context.session, handle.entityUid));
145
+ }
146
+ }
147
+ /** Decodes a draft message handle's accumulated `RopSetProperties` values (`Record<string, string>`, keyed by
148
+ * decimal `PropertyId`) back into typed `CalendarEvent` fields - the write-side mirror of
149
+ * `PropertyResolvers.calendarEventValueFor`'s read-side switch, sharing the same `CalendarNamedProperties.ts`
150
+ * LID table. `PidTagDisplayTo`/`Cc` (not `Bcc` - a meeting has no concept of a blind attendee) become
151
+ * `attendeeAddresses`, matching `RopSubmitMessageHandler`'s own mail-side addressing pragmatic subset (no
152
+ * `RopModifyRecipients` support - see that file's own doc comment).
153
+ *
154
+ * `title` used to default to `""` whenever `PidTagSubject` wasn't present in `properties` at all, rather than
155
+ * staying `undefined` the way every other optional field here already did (the named-property loop below only
156
+ * ever assigns a field when its key is actually present) - `saveAppointment`'s update branch then wrote that
157
+ * `""` straight through with no existing-value fallback, silently blanking the title of any event edited via a
158
+ * `RopSetProperties` call that only touched some other property (e.g. just `PidLidBusyStatus`). Checking `in`
159
+ * explicitly (rather than `properties[key] ?? ""`) preserves the same "present but genuinely empty" vs.
160
+ * "absent" distinction `PidTagSubject` deserves alongside every other field here. */
161
+ function decodeCalendarFieldsFromDraft(session, properties) {
162
+ const subjectKey = String(PID_TAG_SUBJECT);
163
+ const decoded = {
164
+ title: subjectKey in properties ? properties[subjectKey] : undefined,
165
+ attendeeAddresses: [
166
+ ...parseAddressList(properties[String(PID_TAG_DISPLAY_TO)]),
167
+ ...parseAddressList(properties[String(PID_TAG_DISPLAY_CC)]),
168
+ ],
169
+ };
170
+ for (const [key, value] of Object.entries(properties)) {
171
+ const propertyId = Number(key);
172
+ if (propertyId < 0x8000) {
173
+ continue;
174
+ }
175
+ const named = resolveNamedProperty(session, propertyId);
176
+ if (!named || named.kind !== "lid") {
177
+ continue;
178
+ }
179
+ const guid = named.guid.toLowerCase();
180
+ if (guid === PSETID_APPOINTMENT) {
181
+ switch (named.lid) {
182
+ case LID_LOCATION:
183
+ decoded.location = value;
184
+ break;
185
+ case LID_APPOINTMENT_START_WHOLE:
186
+ decoded.startDate = new Date(value);
187
+ break;
188
+ case LID_APPOINTMENT_END_WHOLE:
189
+ decoded.endDate = new Date(value);
190
+ break;
191
+ case LID_BUSY_STATUS:
192
+ decoded.busyStatus = BUSY_STATUS_FROM_CODE[Number(value)] ?? BusyStatus.BUSY;
193
+ break;
194
+ case LID_APPOINTMENT_RECUR:
195
+ decoded.recurrenceRule = decodeAppointmentRecurrence(new BufferReader(Buffer.from(value, "base64")));
196
+ break;
197
+ case LID_TIME_ZONE_STRUCT:
198
+ decoded.timezone = decodeTimeZoneStruct(new BufferReader(Buffer.from(value, "base64")));
199
+ break;
200
+ default:
201
+ break;
202
+ }
203
+ }
204
+ else if (guid === PSETID_COMMON && named.lid === LID_REMINDER_DELTA) {
205
+ decoded.reminderMinutesBeforeStart = Number(value);
206
+ }
207
+ }
208
+ return decoded;
209
+ }
210
+ //# sourceMappingURL=RopSaveChangesMessageHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RopSaveChangesMessageHandler.js","sourceRoot":"","sources":["../../../src/rop/RopSaveChangesMessageHandler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import { readPropertyTag } from "../codec/PropertyValue.js";
2
+ const ROP_ID_SET_COLUMNS = 0x12;
3
+ /** `TBLSTAT_COMPLETE` - this pragmatic subset's tables are always populated synchronously, so `RopSetColumns`
4
+ * (and every other table ROP) never reports any other `TableStatus` value. */
5
+ const TABLE_STATUS_COMPLETE = 0x00;
6
+ /** The well-known MAPI HRESULT `MAPI_E_INVALID_OBJECT`, reused for "the referenced handle isn't a table (or
7
+ * doesn't exist)" - the same constant `RopQueryRowsHandler`/`RopGetHierarchyTableHandler` use for their own
8
+ * analogous checks. */
9
+ const ERROR_INVALID_OBJECT = 0x80070005;
10
+ /**
11
+ * `RopSetColumns` (`[MS-OXCTABL]`/`[MS-OXCROPS]`): configures which properties a table's subsequent
12
+ * `RopQueryRows` calls return, and in what order. Unlike `RopOpenFolder`/`RopGetHierarchyTable`, this ROP
13
+ * doesn't create a new handle - it mutates the existing table handle referenced by `InputHandleIndex` in
14
+ * place, so its response echoes `InputHandleIndex` rather than an `OutputHandleIndex`, the same convention
15
+ * `RopQueryRows`' own response uses (confirmed against `[MS-OXCTABL]`'s real captured response example).
16
+ *
17
+ * Validates that `InputHandleIndex` actually refers to a `"table"` handle before mutating it, the same check
18
+ * every other handle-consuming ROP in this pragmatic subset already makes - a missing or wrong-type handle
19
+ * previously fell through silently (no `else` branch) to a bare success response, meaning a client that set
20
+ * columns against a stale/released/wrong-kind handle index only discovered the real problem one ROP later,
21
+ * when `RopQueryRows` on that same index returned `MAPI_E_INVALID_OBJECT` instead.
22
+ *
23
+ * @author Jean-Philippe Steinmetz
24
+ */
25
+ export class RopSetColumnsHandler {
26
+ constructor() {
27
+ this.ropId = ROP_ID_SET_COLUMNS;
28
+ }
29
+ handle(reader, writer, context) {
30
+ reader.readUInt8(); // LogonId - this pragmatic subset doesn't track multiple concurrent logons per session
31
+ const inputHandleIndex = reader.readUInt8();
32
+ reader.readUInt8(); // SetColumnsFlags - this pragmatic subset has no async/deferred column-set variant
33
+ const propertyTagCount = reader.readUInt16LE();
34
+ const columns = [];
35
+ for (let i = 0; i < propertyTagCount; i++) {
36
+ columns.push(readPropertyTag(reader));
37
+ }
38
+ const handle = context.session.handles[inputHandleIndex];
39
+ if (!handle || handle.type !== "table") {
40
+ writer.writeUInt8(ROP_ID_SET_COLUMNS);
41
+ writer.writeUInt8(inputHandleIndex);
42
+ writer.writeUInt32LE(ERROR_INVALID_OBJECT);
43
+ return;
44
+ }
45
+ handle.columns = columns;
46
+ writer.writeUInt8(ROP_ID_SET_COLUMNS);
47
+ writer.writeUInt8(inputHandleIndex);
48
+ writer.writeUInt32LE(0); // ReturnValue - success
49
+ writer.writeUInt8(TABLE_STATUS_COMPLETE);
50
+ }
51
+ }
52
+ //# sourceMappingURL=RopSetColumnsHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RopSetColumnsHandler.js","sourceRoot":"","sources":["../../../src/rop/RopSetColumnsHandler.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAG5D,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAEhC;8EAC8E;AAC9E,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAEnC;;uBAEuB;AACvB,MAAM,oBAAoB,GAAG,UAAU,CAAC;AAExC;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,oBAAoB;IAAjC;QACoB,UAAK,GAAG,kBAAkB,CAAC;IA0B/C,CAAC;IAxBU,MAAM,CAAC,MAAoB,EAAE,MAAoB,EAAE,OAAmB;QACzE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,uFAAuF;QAC3G,MAAM,gBAAgB,GAAW,MAAM,CAAC,SAAS,EAAE,CAAC;QACpD,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,mFAAmF;QACvG,MAAM,gBAAgB,GAAW,MAAM,CAAC,YAAY,EAAE,CAAC;QACvD,MAAM,OAAO,GAAmD,EAAE,CAAC;QACnE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,gBAAgB,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,OAAO,CAAC,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC;QAC1C,CAAC;QAED,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;QACzD,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YACrC,MAAM,CAAC,UAAU,CAAC,kBAAkB,CAAC,CAAC;YACtC,MAAM,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;YACpC,MAAM,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAC;YAC3C,OAAO;QACX,CAAC;QACD,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC;QAEzB,MAAM,CAAC,UAAU,CAAC,kBAAkB,CAAC,CAAC;QACtC,MAAM,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;QACpC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,wBAAwB;QACjD,MAAM,CAAC,UAAU,CAAC,qBAAqB,CAAC,CAAC;IAC7C,CAAC;CACJ"}
@@ -0,0 +1,162 @@
1
+ import { PropertyType, readTaggedPropertyValue } from "../codec/PropertyValue.js";
2
+ import { LID_APPOINTMENT_END_WHOLE, LID_APPOINTMENT_RECUR, LID_APPOINTMENT_START_WHOLE, LID_BUSY_STATUS, LID_GLOBAL_OBJECT_ID, LID_LOCATION, LID_REMINDER_DELTA, LID_TIME_ZONE_STRUCT, PSETID_APPOINTMENT, PSETID_COMMON, PSETID_MEETING, } from "./CalendarNamedProperties.js";
3
+ import { resolveNamedProperty } from "./NamedPropertyRegistry.js";
4
+ const ROP_ID_SET_PROPERTIES = 0x0a;
5
+ /** The well-known MAPI HRESULT `MAPI_E_INVALID_OBJECT`, reused for "the referenced handle isn't a message (or
6
+ * doesn't exist)" - the same constant `RopGetPropertiesSpecificHandler` uses for its own analogous check.
7
+ * `RopSetProperties` is also spec-valid on Folder/Attachment/Logon objects, none of which this pragmatic
8
+ * subset's compose/send path ever needs to set properties on. */
9
+ const ERROR_INVALID_OBJECT = 0x80070005;
10
+ // The small, well-known set of plain (non-named) properties this pragmatic subset's compose/send path tracks -
11
+ // exactly what RopSubmitMessageHandler/RopSaveChangesMessageHandler need to build a MIME message or persist a
12
+ // CalendarEvent (Subject, an inline small body, the PidTagDisplayTo/Cc/Bcc "cached recipient display string"
13
+ // properties real Outlook also always sets alongside RopModifyRecipients - see RopSubmitMessageHandler's own
14
+ // doc comment for why this pragmatic subset reads addresses from these instead of implementing
15
+ // RopModifyRecipients - and PidTagMessageClass, which RopSaveChangesMessageHandler/RopSubmitMessageHandler
16
+ // branch on to route Calendar items down a different path than ordinary mail). Every other plain property a
17
+ // client sets is accepted (this ROP always reports success, no PropertyProblems) but simply not tracked -
18
+ // harmless for this pragmatic subset's narrow compose/send scope. A *named* property (ID `>= 0x8000`, resolved
19
+ // via `resolveNamedProperty`) is tracked separately below, against the Calendar LID table `PropertyResolvers.ts`
20
+ // also reads from - see `CalendarNamedProperties.ts`.
21
+ const PID_TAG_SUBJECT = 0x0037;
22
+ const PID_TAG_MESSAGE_CLASS = 0x001a;
23
+ const PID_TAG_DISPLAY_BCC = 0x0e02;
24
+ const PID_TAG_DISPLAY_CC = 0x0e03;
25
+ const PID_TAG_DISPLAY_TO = 0x0e04;
26
+ const PID_TAG_BODY = 0x1000;
27
+ /** `PidTagReadReceiptRequested` (`PT_BOOLEAN`) - a client checking "Request a read receipt" before Submit sets
28
+ * this; `RopSubmitMessageHandler` reads it back to attach a real `Disposition-Notification-To` header, mirroring
29
+ * `BaseMessageRoute.send()`'s own explicit-`Message.requestReceipt` handling (see `util/ReceiptUtils.ts`). */
30
+ export const PID_TAG_READ_RECEIPT_REQUESTED = 0x0029;
31
+ /** `PidTagDeferredSendTime` (`PT_SYSTIME`) - real Outlook's "Do not deliver before" compose option;
32
+ * `RopSubmitMessageHandler` reads it back to defer relay exactly like `BaseMessageRoute.send()`'s own
33
+ * `Message.scheduledSendTime` branch does (moving the message to Outbox for `ScheduledSendJob` to relay later)
34
+ * instead of sending immediately. */
35
+ export const PID_TAG_DEFERRED_SEND_TIME = 0x3fef;
36
+ const TRACKED_PROPERTY_IDS = new Set([
37
+ PID_TAG_SUBJECT,
38
+ PID_TAG_MESSAGE_CLASS,
39
+ PID_TAG_DISPLAY_BCC,
40
+ PID_TAG_DISPLAY_CC,
41
+ PID_TAG_DISPLAY_TO,
42
+ PID_TAG_BODY,
43
+ PID_TAG_READ_RECEIPT_REQUESTED,
44
+ PID_TAG_DEFERRED_SEND_TIME,
45
+ ]);
46
+ /** The Calendar named-property LIDs this handler tracks when set under the correct property-set GUID (per
47
+ * `CalendarNamedProperties.ts`) - the write-side mirror of `PropertyResolvers.calendarEventValueFor`'s read-side
48
+ * switch. `PidLidRecurring`/`PidLidReminderSet` are deliberately not tracked here: both are purely-derived
49
+ * boolean flags on the read side (`recurrenceRule !== undefined` / `reminderMinutesBeforeStart != null`), so a
50
+ * client setting them carries no additional information `RopSaveChangesMessageHandler` needs to persist. */
51
+ const TRACKED_APPOINTMENT_LIDS = new Set([
52
+ LID_LOCATION,
53
+ LID_APPOINTMENT_START_WHOLE,
54
+ LID_APPOINTMENT_END_WHOLE,
55
+ LID_BUSY_STATUS,
56
+ LID_APPOINTMENT_RECUR,
57
+ LID_TIME_ZONE_STRUCT,
58
+ ]);
59
+ const TRACKED_COMMON_LIDS = new Set([LID_REMINDER_DELTA]);
60
+ /** `PidLidGlobalObjectId` - the meeting-response side needs this tracked so `RopSubmitMessageHandler` can
61
+ * correlate an incoming `"IPM.Schedule.Meeting.Resp.*"` response back to the `CalendarEvent` it answers, via
62
+ * `MeetingMessageClassHandler.ts`. */
63
+ const TRACKED_MEETING_LIDS = new Set([LID_GLOBAL_OBJECT_ID]);
64
+ /**
65
+ * `RopSetProperties` (`[MS-OXCPRPT]`/`[MS-OXCROPS]`): sets an explicit, client-chosen list of properties
66
+ * (`TaggedPropertyValue`s, each carrying its own `PropertyTag`) on an already-open object. This pragmatic
67
+ * subset supports only `"message"` handles (a `RopCreateMessage` draft, or an already-open `RopOpenMessage`
68
+ * handle) and only tracks the small property set `TRACKED_PROPERTY_IDS`/`TRACKED_APPOINTMENT_LIDS`/
69
+ * `TRACKED_COMMON_LIDS` name - see those constants' own comments for why. A named property (ID `>= 0x8000`) is
70
+ * resolved back to its `(PropertySet GUID, LID)` identity via `resolveNamedProperty` before it can be tracked,
71
+ * since MAPI has no fixed numeric ID for any Calendar property the way `PidTagSubject` has one.
72
+ *
73
+ * Every value is coerced to a plain string via `stringifyValue` before being stored in `handle.draftProperties`
74
+ * (itself a `Record<string, string>` for `MapiSessionContext`'s own JSON-safety reasons, see
75
+ * `MapiSessionManager.ts`) - a lossless, type-aware encoding (ISO-8601 for `PtypTime`, base64 for `PtypBinary`,
76
+ * plain `String()` otherwise) so `RopSaveChangesMessageHandler` can decode a Calendar item's properties back
77
+ * into a real `CalendarEvent`'s typed fields, not just display strings.
78
+ *
79
+ * Always reports success with zero `PropertyProblems`, even for a property this pragmatic subset doesn't
80
+ * track - the same "never fail over an unsupported property" pragmatic stance `RopQueryRows`/
81
+ * `RopGetPropertiesSpecific` already take (falling back to a default value there; simply not tracking the
82
+ * property here, since there is no row to fill in for a *set* operation).
83
+ *
84
+ * @author Jean-Philippe Steinmetz
85
+ */
86
+ export class RopSetPropertiesHandler {
87
+ constructor() {
88
+ this.ropId = ROP_ID_SET_PROPERTIES;
89
+ }
90
+ handle(reader, writer, context) {
91
+ reader.readUInt8(); // LogonId - this pragmatic subset doesn't track multiple concurrent logons per session
92
+ const inputHandleIndex = reader.readUInt8();
93
+ const propertyValueSize = reader.readUInt16LE();
94
+ // PropertyValueSize covers the PropertyValueCount field itself plus PropertyValues, counted from right
95
+ // here (immediately after the PropertyValueSize field, before PropertyValueCount is read).
96
+ const propertyValuesEnd = reader.position + propertyValueSize;
97
+ const propertyValueCount = reader.readUInt16LE();
98
+ const values = [];
99
+ for (let i = 0; i < propertyValueCount; i++) {
100
+ values.push(readTaggedPropertyValue(reader));
101
+ }
102
+ // PropertyValueSize is a byte-count check the request buffer itself provides for framing purposes (so a
103
+ // generic ROP-skipping implementation could skip this ROP without decoding it) - this handler already
104
+ // decoded PropertyValues field-by-field above, so the only remaining use is confirming the reader ended
105
+ // up exactly where PropertyValueSize said it would, catching a malformed request loudly rather than
106
+ // silently misaligning every ROP that follows in the same RopsList.
107
+ if (reader.position !== propertyValuesEnd) {
108
+ throw new Error("RopSetProperties: PropertyValueSize did not match the decoded PropertyValues length.");
109
+ }
110
+ const handle = context.session.handles[inputHandleIndex];
111
+ if (!handle || handle.type !== "message") {
112
+ writer.writeUInt8(ROP_ID_SET_PROPERTIES);
113
+ writer.writeUInt8(inputHandleIndex);
114
+ writer.writeUInt32LE(ERROR_INVALID_OBJECT);
115
+ return;
116
+ }
117
+ handle.draftProperties ?? (handle.draftProperties = {});
118
+ for (const tagged of values) {
119
+ if (TRACKED_PROPERTY_IDS.has(tagged.propertyId)) {
120
+ handle.draftProperties[String(tagged.propertyId)] = stringifyValue(tagged);
121
+ }
122
+ else if (tagged.propertyId >= 0x8000 && this.isTrackedNamedProperty(context, tagged.propertyId)) {
123
+ handle.draftProperties[String(tagged.propertyId)] = stringifyValue(tagged);
124
+ }
125
+ }
126
+ writer.writeUInt8(ROP_ID_SET_PROPERTIES);
127
+ writer.writeUInt8(inputHandleIndex);
128
+ writer.writeUInt32LE(0); // ReturnValue - success
129
+ writer.writeUInt16LE(0); // PropertyProblemCount - never reported, see class doc comment
130
+ }
131
+ isTrackedNamedProperty(context, propertyId) {
132
+ const named = resolveNamedProperty(context.session, propertyId);
133
+ if (!named || named.kind !== "lid" || named.lid === undefined) {
134
+ return false;
135
+ }
136
+ const guid = named.guid.toLowerCase();
137
+ if (guid === PSETID_APPOINTMENT) {
138
+ return TRACKED_APPOINTMENT_LIDS.has(named.lid);
139
+ }
140
+ if (guid === PSETID_COMMON) {
141
+ return TRACKED_COMMON_LIDS.has(named.lid);
142
+ }
143
+ if (guid === PSETID_MEETING) {
144
+ return TRACKED_MEETING_LIDS.has(named.lid);
145
+ }
146
+ return false;
147
+ }
148
+ }
149
+ function stringifyValue(tagged) {
150
+ switch (tagged.propertyType) {
151
+ case PropertyType.PtypString:
152
+ case PropertyType.PtypString8:
153
+ return tagged.value;
154
+ case PropertyType.PtypTime:
155
+ return tagged.value.toISOString();
156
+ case PropertyType.PtypBinary:
157
+ return tagged.value.toString("base64");
158
+ default:
159
+ return String(tagged.value);
160
+ }
161
+ }
162
+ //# sourceMappingURL=RopSetPropertiesHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RopSetPropertiesHandler.js","sourceRoot":"","sources":["../../../src/rop/RopSetPropertiesHandler.ts"],"names":[],"mappings":"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"}