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