@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.
- package/LICENSE +382 -0
- package/README.md +60 -0
- package/dist/lib/BaseMapiEmsmdbRoute.js +297 -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 +119 -0
- package/dist/lib/MapiSessionManager.js.map +1 -0
- package/dist/lib/RopDispatcher.js +34 -0
- package/dist/lib/RopDispatcher.js.map +1 -0
- package/dist/lib/codec/AppointmentRecurrence.js +314 -0
- package/dist/lib/codec/AppointmentRecurrence.js.map +1 -0
- package/dist/lib/codec/BufferCursor.js +213 -0
- package/dist/lib/codec/BufferCursor.js.map +1 -0
- package/dist/lib/codec/GlobalObjectId.js +92 -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 +93 -0
- package/dist/lib/codec/MapiTimeZone.js.map +1 -0
- package/dist/lib/codec/PropertyValue.js +214 -0
- package/dist/lib/codec/PropertyValue.js.map +1 -0
- package/dist/lib/codec/RopBuffer.js +26 -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 +84 -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/CalendarEventTarget.js +27 -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 +23 -0
- package/dist/lib/rop/ContactTarget.js.map +1 -0
- package/dist/lib/rop/FastTransferStream.js +98 -0
- package/dist/lib/rop/FastTransferStream.js.map +1 -0
- package/dist/lib/rop/FolderTarget.js +111 -0
- package/dist/lib/rop/FolderTarget.js.map +1 -0
- package/dist/lib/rop/MeetingMessageClassHandler.js +95 -0
- package/dist/lib/rop/MeetingMessageClassHandler.js.map +1 -0
- package/dist/lib/rop/MessageBodyStream.js +42 -0
- package/dist/lib/rop/MessageBodyStream.js.map +1 -0
- package/dist/lib/rop/MessageTarget.js +44 -0
- package/dist/lib/rop/MessageTarget.js.map +1 -0
- package/dist/lib/rop/NamedPropertyRegistry.js +66 -0
- package/dist/lib/rop/NamedPropertyRegistry.js.map +1 -0
- package/dist/lib/rop/PropertyResolvers.js +313 -0
- package/dist/lib/rop/PropertyResolvers.js.map +1 -0
- package/dist/lib/rop/RopCreateMessageHandler.js +53 -0
- package/dist/lib/rop/RopCreateMessageHandler.js.map +1 -0
- package/dist/lib/rop/RopDeleteFolderHandler.js +133 -0
- package/dist/lib/rop/RopDeleteFolderHandler.js.map +1 -0
- package/dist/lib/rop/RopDeleteMessagesHandler.js +70 -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 +61 -0
- package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js.map +1 -0
- package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js +75 -0
- package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js.map +1 -0
- package/dist/lib/rop/RopGetContentsTableHandler.js +76 -0
- package/dist/lib/rop/RopGetContentsTableHandler.js.map +1 -0
- package/dist/lib/rop/RopGetHierarchyTableHandler.js +40 -0
- package/dist/lib/rop/RopGetHierarchyTableHandler.js.map +1 -0
- package/dist/lib/rop/RopGetPropertiesSpecificHandler.js +55 -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 +2 -0
- package/dist/lib/rop/RopHandler.js.map +1 -0
- package/dist/lib/rop/RopLogonHandler.js +132 -0
- package/dist/lib/rop/RopLogonHandler.js.map +1 -0
- package/dist/lib/rop/RopOpenFolderHandler.js +46 -0
- package/dist/lib/rop/RopOpenFolderHandler.js.map +1 -0
- package/dist/lib/rop/RopOpenMessageHandler.js +81 -0
- package/dist/lib/rop/RopOpenMessageHandler.js.map +1 -0
- package/dist/lib/rop/RopOpenStreamHandler.js +81 -0
- package/dist/lib/rop/RopOpenStreamHandler.js.map +1 -0
- package/dist/lib/rop/RopQueryRowsHandler.js +91 -0
- package/dist/lib/rop/RopQueryRowsHandler.js.map +1 -0
- package/dist/lib/rop/RopReadStreamHandler.js +55 -0
- package/dist/lib/rop/RopReadStreamHandler.js.map +1 -0
- package/dist/lib/rop/RopReleaseHandler.js +20 -0
- package/dist/lib/rop/RopReleaseHandler.js.map +1 -0
- package/dist/lib/rop/RopSaveChangesMessageHandler.js +210 -0
- package/dist/lib/rop/RopSaveChangesMessageHandler.js.map +1 -0
- package/dist/lib/rop/RopSetColumnsHandler.js +52 -0
- package/dist/lib/rop/RopSetColumnsHandler.js.map +1 -0
- package/dist/lib/rop/RopSetPropertiesHandler.js +162 -0
- package/dist/lib/rop/RopSetPropertiesHandler.js.map +1 -0
- package/dist/lib/rop/RopSubmitMessageHandler.js +314 -0
- package/dist/lib/rop/RopSubmitMessageHandler.js.map +1 -0
- package/dist/lib/rop/RopWriteStreamHandler.js +45 -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 +19 -0
- package/dist/lib/rop/TaskTarget.js.map +1 -0
- package/dist/lib/sql/MapiEmsmdbRouteSQL.js +84 -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 +82 -0
- package/dist/types/BaseMapiNspiRoute.d.ts +38 -0
- package/dist/types/MapiSessionManager.d.ts +130 -0
- package/dist/types/RopDispatcher.d.ts +17 -0
- package/dist/types/codec/AppointmentRecurrence.d.ts +14 -0
- package/dist/types/codec/BufferCursor.d.ts +78 -0
- package/dist/types/codec/GlobalObjectId.d.ts +11 -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 +77 -0
- package/dist/types/codec/RopBuffer.d.ts +25 -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 +19 -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 +24 -0
- package/dist/types/rop/CalendarEventTarget.d.ts +29 -0
- package/dist/types/rop/CalendarNamedProperties.d.ts +37 -0
- package/dist/types/rop/ContactTarget.d.ts +29 -0
- package/dist/types/rop/FastTransferStream.d.ts +18 -0
- package/dist/types/rop/FolderTarget.d.ts +63 -0
- package/dist/types/rop/MeetingMessageClassHandler.d.ts +36 -0
- package/dist/types/rop/MessageBodyStream.d.ts +26 -0
- package/dist/types/rop/MessageTarget.d.ts +38 -0
- package/dist/types/rop/NamedPropertyRegistry.d.ts +46 -0
- package/dist/types/rop/PropertyResolvers.d.ts +60 -0
- package/dist/types/rop/RopCreateMessageHandler.d.ts +23 -0
- package/dist/types/rop/RopDeleteFolderHandler.d.ts +38 -0
- package/dist/types/rop/RopDeleteMessagesHandler.d.ts +31 -0
- package/dist/types/rop/RopFastTransferSourceCopyPropertiesHandler.d.ts +24 -0
- package/dist/types/rop/RopFastTransferSourceCopyToHandler.d.ts +24 -0
- package/dist/types/rop/RopFastTransferSourceGetBufferHandler.d.ts +23 -0
- package/dist/types/rop/RopGetContentsTableHandler.d.ts +31 -0
- package/dist/types/rop/RopGetHierarchyTableHandler.d.ts +17 -0
- package/dist/types/rop/RopGetPropertiesSpecificHandler.d.ts +23 -0
- package/dist/types/rop/RopGetPropertyIdsFromNamesHandler.d.ts +23 -0
- package/dist/types/rop/RopHandler.d.ts +67 -0
- package/dist/types/rop/RopLogonHandler.d.ts +31 -0
- package/dist/types/rop/RopOpenFolderHandler.d.ts +19 -0
- package/dist/types/rop/RopOpenMessageHandler.d.ts +27 -0
- package/dist/types/rop/RopOpenStreamHandler.d.ts +27 -0
- package/dist/types/rop/RopQueryRowsHandler.d.ts +36 -0
- package/dist/types/rop/RopReadStreamHandler.d.ts +18 -0
- package/dist/types/rop/RopReleaseHandler.d.ts +14 -0
- package/dist/types/rop/RopSaveChangesMessageHandler.d.ts +44 -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 +70 -0
- package/dist/types/rop/RopWriteStreamHandler.d.ts +21 -0
- package/dist/types/rop/TaskNamedProperties.d.ts +24 -0
- package/dist/types/rop/TaskTarget.d.ts +24 -0
- package/dist/types/sql/MapiEmsmdbRouteSQL.d.ts +19 -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,98 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { FolderType } from "@rapidmx/restapi";
|
|
6
|
+
import { BufferWriter } from "../codec/BufferCursor.js";
|
|
7
|
+
import { PropertyType, writeTaggedPropertyValue } from "../codec/PropertyValue.js";
|
|
8
|
+
import { resolveFolderCalendarEvents } from "./CalendarEventTarget.js";
|
|
9
|
+
import { resolveFolderMessages } from "./MessageTarget.js";
|
|
10
|
+
import { resolvePropertyValues } from "./PropertyResolvers.js";
|
|
11
|
+
/**
|
|
12
|
+
* Builds the `FastTransfer` binary stream (`[MS-OXCFXICS]` §2.2.4) `RopFastTransferSourceCopyTo`/
|
|
13
|
+
* `CopyProperties` hand off to `RopFastTransferSourceGetBuffer` for paging out, per this pragmatic subset's own
|
|
14
|
+
* "ICS" scope (see the architecture plan's "Incremental sync" row): a **full, non-differential** dump of the
|
|
15
|
+
* target's current live state, built eagerly and stored whole on the output handle - not the real, much larger
|
|
16
|
+
* `contentsSync`/`hierarchySync` grammar (`IncrSyncChg`/`IncrSyncDel`/`IncrSyncStateBegin` markers), which
|
|
17
|
+
* requires a persisted per-device ICS state object (an `IDSET` baseline) this pragmatic subset doesn't
|
|
18
|
+
* implement. A real client that asks for an incremental sync here simply gets a full re-list every time and
|
|
19
|
+
* reconciles by diffing against its own local cache - spec-legitimate client behavior, just more state
|
|
20
|
+
* re-fetched per sync than a byte-perfect ICS server would send, exactly the tradeoff the plan accepts.
|
|
21
|
+
*
|
|
22
|
+
* Only the plain `PidTag*` properties this pragmatic subset already resolves via `PropertyResolvers.ts` are
|
|
23
|
+
* ever included - a Calendar item's own named properties (start/end/location/recurrence/...) are **not**
|
|
24
|
+
* included in the stream (only `PidTagSubject`), a deliberate scope cut: a client wanting those already has to
|
|
25
|
+
* `RopOpenMessage`+`RopGetPropertiesSpecific` the item directly (the same "just re-fetches more state" tradeoff
|
|
26
|
+
* applied one level deeper). No recipients/attachments/subfolders are emitted either - `messageChildren` is
|
|
27
|
+
* always empty and `Level` (whether to recurse into subfolders) is not honored, matching this pragmatic
|
|
28
|
+
* subset's existing "no `RopModifyRecipients`" and folder-hierarchy-is-flat-per-call precedents elsewhere.
|
|
29
|
+
*
|
|
30
|
+
* @author Jean-Philippe Steinmetz
|
|
31
|
+
*/
|
|
32
|
+
/** FastTransfer markers (`[MS-OXCFXICS]` §2.2.4.1.4, confirmed this session) - only the four this pragmatic
|
|
33
|
+
* subset's flat, non-differential dump ever needs. */
|
|
34
|
+
const MARKER_START_TOP_FLD = 0x40090003;
|
|
35
|
+
const MARKER_END_FOLDER = 0x400b0003;
|
|
36
|
+
const MARKER_START_MESSAGE = 0x400c0003;
|
|
37
|
+
const MARKER_END_MESSAGE = 0x400d0003;
|
|
38
|
+
/** The well-known columns this pragmatic subset includes by default when the caller (`RopFastTransferSourceCopyTo`)
|
|
39
|
+
* didn't supply an explicit include list - the same small "what a real client needs" sets `PropertyResolvers.ts`
|
|
40
|
+
* already defines its own defaults around. */
|
|
41
|
+
const DEFAULT_FOLDER_COLUMNS = [
|
|
42
|
+
{ propertyId: 0x3001, propertyType: PropertyType.PtypString }, // PidTagDisplayName
|
|
43
|
+
{ propertyId: 0x3602, propertyType: PropertyType.PtypInteger32 }, // PidTagContentCount
|
|
44
|
+
{ propertyId: 0x3603, propertyType: PropertyType.PtypInteger32 }, // PidTagContentUnreadCount
|
|
45
|
+
];
|
|
46
|
+
const DEFAULT_MESSAGE_COLUMNS = [
|
|
47
|
+
{ propertyId: 0x0037, propertyType: PropertyType.PtypString }, // PidTagSubject
|
|
48
|
+
{ propertyId: 0x0e07, propertyType: PropertyType.PtypInteger32 }, // PidTagMessageFlags
|
|
49
|
+
{ propertyId: 0x0e1b, propertyType: PropertyType.PtypBoolean }, // PidTagHasAttachments
|
|
50
|
+
{ propertyId: 0x0e06, propertyType: PropertyType.PtypTime }, // PidTagMessageDeliveryTime
|
|
51
|
+
];
|
|
52
|
+
/** A calendar item's named properties are deliberately excluded from the stream (see class doc comment) -
|
|
53
|
+
* `PidTagSubject` is the only column emitted for one. */
|
|
54
|
+
const DEFAULT_CALENDAR_COLUMNS = [{ propertyId: 0x0037, propertyType: PropertyType.PtypString }];
|
|
55
|
+
async function writePropList(writer, target, columns, context) {
|
|
56
|
+
const values = await resolvePropertyValues(target, columns, context);
|
|
57
|
+
columns.forEach((column, index) => writeTaggedPropertyValue(writer, { propertyId: column.propertyId, propertyType: column.propertyType, value: values[index] }));
|
|
58
|
+
}
|
|
59
|
+
function filterExcluded(columns, excludePropertyIds) {
|
|
60
|
+
return columns.filter((column) => !excludePropertyIds.has(column.propertyId));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Builds the complete FastTransfer stream for `handle` (a `"folder"` or `"message"` Server object). `columns`,
|
|
64
|
+
* when given (a `RopFastTransferSourceCopyProperties` explicit include list), replaces every default column set
|
|
65
|
+
* uniformly at both folder- and message-level; `excludePropertyIds` (a `RopFastTransferSourceCopyTo` exclude
|
|
66
|
+
* list) is only ever applied to this pragmatic subset's own default columns, per the same reasoning.
|
|
67
|
+
*/
|
|
68
|
+
export async function buildFastTransferStream(handle, context, options = {}) {
|
|
69
|
+
const writer = new BufferWriter();
|
|
70
|
+
const exclude = options.excludePropertyIds ?? new Set();
|
|
71
|
+
if (handle.type === "folder") {
|
|
72
|
+
const folderColumns = options.columns ?? filterExcluded(DEFAULT_FOLDER_COLUMNS, exclude);
|
|
73
|
+
writer.writeUInt32LE(MARKER_START_TOP_FLD);
|
|
74
|
+
await writePropList(writer, handle.entityUid, folderColumns, context);
|
|
75
|
+
if (handle.entityUid.startsWith("folder:")) {
|
|
76
|
+
const folderUid = handle.entityUid.slice("folder:".length);
|
|
77
|
+
const folder = await context.folderRepo.findOne(folderUid, { ignoreACL: true });
|
|
78
|
+
const isCalendar = folder?.type === FolderType.CALENDAR;
|
|
79
|
+
const rows = isCalendar
|
|
80
|
+
? await resolveFolderCalendarEvents(folderUid, context.calendarEventRepo)
|
|
81
|
+
: await resolveFolderMessages(folderUid, context.messageRepo);
|
|
82
|
+
const messageColumns = options.columns ?? filterExcluded(isCalendar ? DEFAULT_CALENDAR_COLUMNS : DEFAULT_MESSAGE_COLUMNS, exclude);
|
|
83
|
+
for (const row of rows) {
|
|
84
|
+
writer.writeUInt32LE(MARKER_START_MESSAGE);
|
|
85
|
+
await writePropList(writer, row, messageColumns, context);
|
|
86
|
+
writer.writeUInt32LE(MARKER_END_MESSAGE);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
writer.writeUInt32LE(MARKER_END_FOLDER);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const isCalendar = handle.entityUid.startsWith("calendarEvent:");
|
|
93
|
+
const columns = options.columns ?? filterExcluded(isCalendar ? DEFAULT_CALENDAR_COLUMNS : DEFAULT_MESSAGE_COLUMNS, exclude);
|
|
94
|
+
await writePropList(writer, handle.entityUid, columns, context);
|
|
95
|
+
}
|
|
96
|
+
return writer.toBuffer();
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=FastTransferStream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FastTransferStream.js","sourceRoot":"","sources":["../../../src/rop/FastTransferStream.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,EAAU,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAEnF,OAAO,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAG/D;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;sDACsD;AACtD,MAAM,oBAAoB,GAAG,UAAU,CAAC;AACxC,MAAM,iBAAiB,GAAG,UAAU,CAAC;AACrC,MAAM,oBAAoB,GAAG,UAAU,CAAC;AACxC,MAAM,kBAAkB,GAAG,UAAU,CAAC;AAOtC;;8CAE8C;AAC9C,MAAM,sBAAsB,GAAqB;IAC7C,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,CAAC,UAAU,EAAE,EAAE,oBAAoB;IACnF,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,CAAC,aAAa,EAAE,EAAE,qBAAqB;IACvF,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,CAAC,aAAa,EAAE,EAAE,2BAA2B;CAChG,CAAC;AACF,MAAM,uBAAuB,GAAqB;IAC9C,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,CAAC,UAAU,EAAE,EAAE,gBAAgB;IAC/E,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,CAAC,aAAa,EAAE,EAAE,qBAAqB;IACvF,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,CAAC,WAAW,EAAE,EAAE,uBAAuB;IACvF,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,CAAC,QAAQ,EAAE,EAAE,4BAA4B;CAC5F,CAAC;AACF;yDACyD;AACzD,MAAM,wBAAwB,GAAqB,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,CAAC,UAAU,EAAE,CAAC,CAAC;AAEnH,KAAK,UAAU,aAAa,CACxB,MAAoB,EACpB,MAAc,EACd,OAAyB,EACzB,OAAwH;IAExH,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IACrE,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAC9B,wBAAwB,CAAC,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAC/H,CAAC;AACN,CAAC;AAED,SAAS,cAAc,CAAC,OAAyB,EAAE,kBAAuC;IACtF,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;AAClF,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CACzC,MAAwB,EACxB,OAAmB,EACnB,UAAoF,EAAE;IAEtF,MAAM,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;IAClC,MAAM,OAAO,GAAG,OAAO,CAAC,kBAAkB,IAAI,IAAI,GAAG,EAAU,CAAC;IAEhE,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC3B,MAAM,aAAa,GAAG,OAAO,CAAC,OAAO,IAAI,cAAc,CAAC,sBAAsB,EAAE,OAAO,CAAC,CAAC;QACzF,MAAM,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAC;QAC3C,MAAM,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,aAAa,EAAE,OAAO,CAAC,CAAC;QAEtE,IAAI,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YACzC,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;YAC3D,MAAM,MAAM,GAAuB,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACpG,MAAM,UAAU,GAAG,MAAM,EAAE,IAAI,KAAK,UAAU,CAAC,QAAQ,CAAC;YACxD,MAAM,IAAI,GAAG,UAAU;gBACnB,CAAC,CAAC,MAAM,2BAA2B,CAAC,SAAS,EAAE,OAAO,CAAC,iBAAiB,CAAC;gBACzE,CAAC,CAAC,MAAM,qBAAqB,CAAC,SAAS,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;YAClE,MAAM,cAAc,GAAG,OAAO,CAAC,OAAO,IAAI,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,uBAAuB,EAAE,OAAO,CAAC,CAAC;YAEnI,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;gBACrB,MAAM,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAC;gBAC3C,MAAM,aAAa,CAAC,MAAM,EAAE,GAAG,EAAE,cAAc,EAAE,OAAO,CAAC,CAAC;gBAC1D,MAAM,CAAC,aAAa,CAAC,kBAAkB,CAAC,CAAC;YAC7C,CAAC;QACL,CAAC;QAED,MAAM,CAAC,aAAa,CAAC,iBAAiB,CAAC,CAAC;IAC5C,CAAC;SAAM,CAAC;QACJ,MAAM,UAAU,GAAG,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;QACjE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,uBAAuB,EAAE,OAAO,CAAC,CAAC;QAC5H,MAAM,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IACpE,CAAC;IAED,OAAO,MAAM,CAAC,QAAQ,EAAE,CAAC;AAC7B,CAAC"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for resolving the `"virtual:<name>"`/`"folder:<uid>"` target strings `RopLogonHandler`
|
|
3
|
+
* assigns to FIDs (see its own doc comment) into real folder data - used by `RopOpenFolderHandler` and
|
|
4
|
+
* `RopGetHierarchyTableHandler`.
|
|
5
|
+
*/
|
|
6
|
+
/** Display names for the virtual (no real backing `Folder`) special folders - see `RopLogonHandler`'s own
|
|
7
|
+
* doc comment for why these don't have real rows in this data model. */
|
|
8
|
+
const VIRTUAL_DISPLAY_NAMES = {
|
|
9
|
+
root: "Root",
|
|
10
|
+
deferredAction: "Deferred Action",
|
|
11
|
+
spoolerQueue: "Spooler Queue",
|
|
12
|
+
ipmSubtree: "Top of Information Store",
|
|
13
|
+
commonViews: "Common Views",
|
|
14
|
+
schedule: "Schedule",
|
|
15
|
+
search: "Finder",
|
|
16
|
+
views: "Views",
|
|
17
|
+
shortcuts: "Shortcuts",
|
|
18
|
+
};
|
|
19
|
+
/** Resolves a target string into the display data a folder-table row needs. A `"folder:<uid>"` target whose
|
|
20
|
+
* `Folder` has since been deleted (soft-deleted or otherwise vanished) degrades to empty-looking values rather
|
|
21
|
+
* than throwing - the row simply won't be interesting to a client, not a reason to fail the whole ROP. */
|
|
22
|
+
export async function resolveFolderInfo(mailboxUid, target, folderRepo, cache) {
|
|
23
|
+
if (target.startsWith("folder:")) {
|
|
24
|
+
const uid = target.slice("folder:".length);
|
|
25
|
+
const folder = await folderRepo.findOne(uid, { ignoreACL: true });
|
|
26
|
+
const hasChildren = (await resolveFolderChildren(mailboxUid, target, folderRepo, cache)).length > 0;
|
|
27
|
+
return {
|
|
28
|
+
displayName: folder?.name ?? "",
|
|
29
|
+
unreadCount: folder?.unreadCount ?? 0,
|
|
30
|
+
totalCount: folder?.totalCount ?? 0,
|
|
31
|
+
hasChildren,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const name = target.slice("virtual:".length);
|
|
35
|
+
const hasChildren = (await resolveFolderChildren(mailboxUid, target, folderRepo, cache)).length > 0;
|
|
36
|
+
return { displayName: VIRTUAL_DISPLAY_NAMES[name] ?? name, unreadCount: 0, totalCount: 0, hasChildren };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolves the direct children of `target`, as an array of the same target-string format, for
|
|
40
|
+
* `RopGetHierarchyTable`. Only `"virtual:root"`/`"virtual:ipmSubtree"` (this mailbox's top-level real
|
|
41
|
+
* folders - both collapse to the same "top of the visible tree" concept in this pragmatic subset, see
|
|
42
|
+
* `RopLogonHandler`'s own doc comment) and a real folder (its own real children) have any children at all;
|
|
43
|
+
* every other virtual folder (Deferred Action, Spooler Queue, ...) is permanently empty, since this data
|
|
44
|
+
* model has no concept of nesting anything under them.
|
|
45
|
+
*
|
|
46
|
+
* Filters an already-fetched full folder list in application code rather than querying by
|
|
47
|
+
* `parentFolderUid: undefined` directly - query-DSL semantics for "field is unset" aren't reliably consistent
|
|
48
|
+
* across backends (the exact kind of gap this project's own testing philosophy has caught before, e.g. `$or`
|
|
49
|
+
* being Mongo-only), so filtering a fetched array sidesteps the question entirely rather than risking it.
|
|
50
|
+
* The comparison itself uses `== null` (matching both `null` and `undefined`), not `=== undefined` - a
|
|
51
|
+
* top-level folder's unset `parentFolderUid` round-trips as genuine `undefined` from Mongo but as `null` from
|
|
52
|
+
* a SQL `nullable` column, a real, previously-confirmed cross-backend discrepancy in this codebase (caught by
|
|
53
|
+
* this exact test against a real SQLite-backed server, not assumed).
|
|
54
|
+
*
|
|
55
|
+
* **`cache`**: without it, this fetches every folder in the mailbox on every call - fine for the single call
|
|
56
|
+
* `RopGetHierarchyTableHandler` makes to build a table, but `resolveFolderInfo` also calls this once per row
|
|
57
|
+
* just to compute `hasChildren`, and a table page can hold many rows. A caller resolving a whole page (
|
|
58
|
+
* `RopQueryRowsHandler`/`RopGetPropertiesSpecificHandler`) passes one shared cache object through every call in
|
|
59
|
+
* that batch so the full list is fetched at most once per batch instead of once per row - see
|
|
60
|
+
* `FolderResolutionCache`'s own doc comment for why this isn't session state.
|
|
61
|
+
*/
|
|
62
|
+
export async function resolveFolderChildren(mailboxUid, target, folderRepo, cache) {
|
|
63
|
+
const isTopLevel = target === "virtual:root" || target === "virtual:ipmSubtree";
|
|
64
|
+
let parentFolderUid;
|
|
65
|
+
if (isTopLevel) {
|
|
66
|
+
parentFolderUid = undefined;
|
|
67
|
+
}
|
|
68
|
+
else if (target.startsWith("folder:")) {
|
|
69
|
+
parentFolderUid = target.slice("folder:".length);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
let allFolders;
|
|
75
|
+
if (cache) {
|
|
76
|
+
cache.allFolders ?? (cache.allFolders = await folderRepo.find({ mailboxUid }, { ignoreACL: true }));
|
|
77
|
+
allFolders = cache.allFolders;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
allFolders = await folderRepo.find({ mailboxUid }, { ignoreACL: true });
|
|
81
|
+
}
|
|
82
|
+
return allFolders
|
|
83
|
+
.filter((f) => (isTopLevel ? f.parentFolderUid == null : f.parentFolderUid === parentFolderUid))
|
|
84
|
+
.map((f) => `folder:${f.uid}`);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Returns `target`'s existing FID if `RopLogon` or an earlier `RopGetHierarchyTable` row already assigned
|
|
88
|
+
* one, otherwise assigns and remembers the next free small integer FID. This is what lets a client
|
|
89
|
+
* `RopOpenFolder` a child folder it only ever learned about via a `RopQueryRows` row's `PidTagFolderId`
|
|
90
|
+
* column - without this, only the 13 `RopLogon`-time special folders could ever be opened.
|
|
91
|
+
*
|
|
92
|
+
* Backed by `session.folderTargetIds` (target -> FID) and `session.nextFolderId`, an O(1) reverse index/counter
|
|
93
|
+
* pair rather than a linear scan of `session.folderIds` plus a `Math.max(...spread)` over its keys - both real
|
|
94
|
+
* costs, not just theoretical ones: `folderTargetIds` only ever grows for a session's lifetime as a client
|
|
95
|
+
* browses more of a mailbox's folder tree, so the old scan-per-lookup approach was quadratic across a session
|
|
96
|
+
* that opens many folders, and `Math.max` spreading an unbounded array as call arguments risked a stack
|
|
97
|
+
* overflow past V8's argument-count limit on a mailbox with enough folders. `RopLogonHandler` keeps
|
|
98
|
+
* `folderTargetIds`/`nextFolderId` in sync with `folderIds` for the 13 special folders it assigns directly,
|
|
99
|
+
* rather than going through this function, since it assigns all 13 as one batch up front.
|
|
100
|
+
*/
|
|
101
|
+
export function assignOrGetFid(session, target) {
|
|
102
|
+
const existing = session.folderTargetIds[target];
|
|
103
|
+
if (existing !== undefined) {
|
|
104
|
+
return existing;
|
|
105
|
+
}
|
|
106
|
+
const fid = session.nextFolderId++;
|
|
107
|
+
session.folderIds[String(fid)] = target;
|
|
108
|
+
session.folderTargetIds[target] = fid;
|
|
109
|
+
return fid;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=FolderTarget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FolderTarget.js","sourceRoot":"","sources":["../../../src/rop/FolderTarget.ts"],"names":[],"mappings":"AAQA;;;;GAIG;AAEH;wEACwE;AACxE,MAAM,qBAAqB,GAA2B;IAClD,IAAI,EAAE,MAAM;IACZ,cAAc,EAAE,iBAAiB;IACjC,YAAY,EAAE,eAAe;IAC7B,UAAU,EAAE,0BAA0B;IACtC,WAAW,EAAE,cAAc;IAC3B,QAAQ,EAAE,UAAU;IACpB,MAAM,EAAE,QAAQ;IAChB,KAAK,EAAE,OAAO;IACd,SAAS,EAAE,WAAW;CACzB,CAAC;AAmBF;;0GAE0G;AAC1G,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACnC,UAAkB,EAClB,MAAc,EACd,UAA0B,EAC1B,KAA6B;IAE7B,IAAI,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC/B,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC3C,MAAM,MAAM,GAAuB,MAAM,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACtF,MAAM,WAAW,GAAG,CAAC,MAAM,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;QACpG,OAAO;YACH,WAAW,EAAE,MAAM,EAAE,IAAI,IAAI,EAAE;YAC/B,WAAW,EAAE,MAAM,EAAE,WAAW,IAAI,CAAC;YACrC,UAAU,EAAE,MAAM,EAAE,UAAU,IAAI,CAAC;YACnC,WAAW;SACd,CAAC;IACN,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IAC7C,MAAM,WAAW,GAAG,CAAC,MAAM,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;IACpG,OAAO,EAAE,WAAW,EAAE,qBAAqB,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,WAAW,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC;AAC5G,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACvC,UAAkB,EAClB,MAAc,EACd,UAA0B,EAC1B,KAA6B;IAE7B,MAAM,UAAU,GAAG,MAAM,KAAK,cAAc,IAAI,MAAM,KAAK,oBAAoB,CAAC;IAChF,IAAI,eAAmC,CAAC;IACxC,IAAI,UAAU,EAAE,CAAC;QACb,eAAe,GAAG,SAAS,CAAC;IAChC,CAAC;SAAM,IAAI,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACtC,eAAe,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACrD,CAAC;SAAM,CAAC;QACJ,OAAO,EAAE,CAAC;IACd,CAAC;IAED,IAAI,UAAoB,CAAC;IACzB,IAAI,KAAK,EAAE,CAAC;QACR,KAAK,CAAC,UAAU,KAAhB,KAAK,CAAC,UAAU,GAAK,MAAM,UAAU,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAC;QAChF,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC;IAClC,CAAC;SAAM,CAAC;QACJ,UAAU,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO,UAAU;SACZ,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,KAAK,eAAe,CAAC,CAAC;SAC/F,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,cAAc,CAAC,OAA2B,EAAE,MAAc;IACtE,MAAM,QAAQ,GAAG,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;IACjD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,QAAQ,CAAC;IACpB,CAAC;IACD,MAAM,GAAG,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IACnC,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC;IACxC,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC;IACtC,OAAO,GAAG,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { AttendeeResponseStatus } from "@rapidmx/restapi";
|
|
6
|
+
import { BufferReader } from "../codec/BufferCursor.js";
|
|
7
|
+
import { decodeGlobalObjectId } from "../codec/GlobalObjectId.js";
|
|
8
|
+
import { LID_GLOBAL_OBJECT_ID, PSETID_MEETING } from "./CalendarNamedProperties.js";
|
|
9
|
+
import { resolveNamedProperty } from "./NamedPropertyRegistry.js";
|
|
10
|
+
/** `PidTagMessageClass` suffix -> the caller's own new `AttendeeResponseStatus`, per `[MS-OXOCAL]`'s meeting
|
|
11
|
+
* response object naming convention. An unrecognized suffix (a message class this pragmatic subset doesn't
|
|
12
|
+
* know how to interpret as a response) is simply not handled - see `submitMeetingResponse`'s own doc comment. */
|
|
13
|
+
const RESPONSE_STATUS_BY_MESSAGE_CLASS = {
|
|
14
|
+
"IPM.Schedule.Meeting.Resp.Pos": AttendeeResponseStatus.ACCEPTED,
|
|
15
|
+
"IPM.Schedule.Meeting.Resp.Tent": AttendeeResponseStatus.TENTATIVE,
|
|
16
|
+
"IPM.Schedule.Meeting.Resp.Neg": AttendeeResponseStatus.DECLINED,
|
|
17
|
+
};
|
|
18
|
+
/** Scans `properties` (a draft handle's accumulated `RopSetProperties` values) for the one already-assigned
|
|
19
|
+
* named property matching `(guid, lid)`, returning its raw stored string value - the single-property analog of
|
|
20
|
+
* `RopSaveChangesMessageHandler.ts`'s own full-table decode loop, needed here only for `PidLidGlobalObjectId`. */
|
|
21
|
+
function findNamedPropertyValue(session, properties, guid, lid) {
|
|
22
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
23
|
+
const propertyId = Number(key);
|
|
24
|
+
if (propertyId < 0x8000) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const named = resolveNamedProperty(session, propertyId);
|
|
28
|
+
if (named && named.kind === "lid" && named.lid === lid && named.guid.toLowerCase() === guid) {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Handles a submitted `"IPM.Schedule.Meeting.Resp.{Pos,Neg,Tent}"` message - an attendee's own response to a
|
|
36
|
+
* meeting invite this server previously sent (`RopSubmitMessageHandler.submitAppointment`). Delegated to from
|
|
37
|
+
* `RopSubmitMessageHandler` for that message-class prefix, instead of the ordinary mail or Appointment paths.
|
|
38
|
+
*
|
|
39
|
+
* Correlates the response back to the original `CalendarEvent` via `PidLidGlobalObjectId` (echoed by a real
|
|
40
|
+
* client from the invite it's responding to - see `GlobalObjectId.ts`'s own doc comment for why this requires
|
|
41
|
+
* no separate correlation table) and updates the caller's own `Attendee.responseStatus` in place, reusing the
|
|
42
|
+
* exact same matching-by-mailbox-address logic `MeetingResponseCommand` (EAS's own `MeetingResponse` handler)
|
|
43
|
+
* already implements - this is a from-scratch reimplementation of that small algorithm rather than a
|
|
44
|
+
* cross-import, since EAS and MAPI are independent protocol verticals in this codebase with no dependency
|
|
45
|
+
* between them.
|
|
46
|
+
*
|
|
47
|
+
* Every failure mode here (no `PidLidGlobalObjectId` set, no matching `CalendarEvent`, no resolvable mailbox,
|
|
48
|
+
* caller isn't actually an attendee, unrecognized message-class suffix) is a silent no-op rather than an error
|
|
49
|
+
* response - the same "don't fail the whole ROP over a stale/unresolvable reference" principle this pragmatic
|
|
50
|
+
* subset applies throughout (e.g. `MessageTarget.resolveMessageInfo`'s own doc comment). A response that can't
|
|
51
|
+
* be correlated or applied is simply dropped, not a reason to reject the client's `RopSubmitMessage` call.
|
|
52
|
+
*
|
|
53
|
+
* **The one query in this package that isn't `mailboxUid`-scoped, and why that's unavoidable here**: unlike
|
|
54
|
+
* every other `ignoreACL: true` repo call in this codebase (all transitively scoped to the caller's own
|
|
55
|
+
* mailbox via a session FID/MID registry that itself only ever gets populated from mailbox-scoped queries),
|
|
56
|
+
* `calendarEventRepo.find({ icalUid })` below deliberately searches *every* mailbox on this server - the
|
|
57
|
+
* `CalendarEvent` being responded to belongs to the meeting's *organizer*, who is very often a different
|
|
58
|
+
* mailbox than the attendee submitting this response, and the caller's own session has no way to know which
|
|
59
|
+
* mailbox that is. Safety instead comes from two independent properties: `icalUid` is a server-generated
|
|
60
|
+
* `crypto.randomUUID()` (see `RopSaveChangesMessageHandler.saveAppointment`), for all practical purposes
|
|
61
|
+
* globally unique, so this can't be used to enumerate or collide with another organizer's events; and the
|
|
62
|
+
* actual authorization gate is the attendee-membership check a few lines down (`callerAddresses.has(...)`),
|
|
63
|
+
* derived entirely from *this* caller's own resolved mailbox record, not from anything the client supplied -
|
|
64
|
+
* a crafted `PidLidGlobalObjectId` can at best name a real `icalUid` it doesn't already know the value of
|
|
65
|
+
* (astronomically unlikely against a random UUID) and still can't mutate an event unless the caller's own
|
|
66
|
+
* mailbox is genuinely a listed attendee on it.
|
|
67
|
+
*/
|
|
68
|
+
export async function submitMeetingResponse(messageClass, draftProperties, context) {
|
|
69
|
+
const responseStatus = RESPONSE_STATUS_BY_MESSAGE_CLASS[messageClass];
|
|
70
|
+
if (!responseStatus) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const globalObjectIdBase64 = findNamedPropertyValue(context.session, draftProperties, PSETID_MEETING, LID_GLOBAL_OBJECT_ID);
|
|
74
|
+
if (!globalObjectIdBase64) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const icalUid = decodeGlobalObjectId(new BufferReader(Buffer.from(globalObjectIdBase64, "base64")));
|
|
78
|
+
const events = await context.calendarEventRepo.find({ icalUid }, { ignoreACL: true, limit: 1 });
|
|
79
|
+
const event = events[0];
|
|
80
|
+
if (!event) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const mailbox = await context.mailboxRepo.findOne(context.mailboxUid, { ignoreACL: true });
|
|
84
|
+
if (!mailbox) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const callerAddresses = new Set([mailbox.primarySmtpAddress.toLowerCase(), ...mailbox.aliasAddresses.map((a) => a.toLowerCase())]);
|
|
88
|
+
const attendeeIndex = event.attendees.findIndex((attendee) => callerAddresses.has(attendee.address.toLowerCase()));
|
|
89
|
+
if (attendeeIndex === -1) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const attendees = event.attendees.map((attendee, i) => (i === attendeeIndex ? { ...attendee, responseStatus } : attendee));
|
|
93
|
+
await context.calendarEventRepo.update({ uid: event.uid, version: event.version, attendees }, event, { ignoreACL: true });
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=MeetingMessageClassHandler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MeetingMessageClassHandler.js","sourceRoot":"","sources":["../../../src/rop/MeetingMessageClassHandler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { simpleParser } from "mailparser";
|
|
6
|
+
import { BufferWriter } from "../codec/BufferCursor.js";
|
|
7
|
+
/** `PidTagBody` (`[MS-OXPROPS]`): property ID `0x1000`, `PtypString`. The only streamable property this
|
|
8
|
+
* pragmatic subset supports via `RopOpenStream`/`RopReadStream` - `PidTagHtml`/`PidTagRtfCompressed` are a
|
|
9
|
+
* documented gap (see `RopOpenStreamHandler`'s own doc comment). */
|
|
10
|
+
export const PID_TAG_BODY = 0x1000;
|
|
11
|
+
/**
|
|
12
|
+
* Resolves a `"message:<uid>"` target's plain-text body as the exact byte sequence `RopReadStream` must serve
|
|
13
|
+
* for a streamed `PtypString` property - confirmed via `[MS-OXCPRPT]`'s `RopOpenStream` page ("a string of
|
|
14
|
+
* Unicode characters in UTF-16LE format encoding with terminating null character"), the identical encoding
|
|
15
|
+
* `writePropertyValue()` already uses for an inline `PtypString` value, so this reuses
|
|
16
|
+
* `BufferWriter.writeNullTerminatedUtf16LE` rather than reimplementing it.
|
|
17
|
+
*
|
|
18
|
+
* Always derived from the message's raw MIME source (`bodyBlobKey`, via `mailparser` - the same fallback path
|
|
19
|
+
* `ItemOperationsCommand.fetchMessage()` already uses for EAS's own plain-text body delivery) rather than
|
|
20
|
+
* `sanitizedHtmlBlobKey` - `PidTagBody` is specifically the plain-text body per `[MS-OXPROPS]`, unlike EAS's
|
|
21
|
+
* `Body` element, which can carry either format tagged by its own `Type` field.
|
|
22
|
+
*
|
|
23
|
+
* Recomputed on every call rather than cached anywhere (including on the `"stream"` handle itself) -
|
|
24
|
+
* `MapiSessionContext` is serialized through `RedisCache`'s JSON round trip for multi-instance deployments, so
|
|
25
|
+
* a raw `Buffer` stored there wouldn't survive it (the same class of gap this codebase already documented for
|
|
26
|
+
* `Date` fields, see `MapiSessionContext`'s own doc comment). A documented, pragmatic trade-off: a large body
|
|
27
|
+
* read across several `RopReadStream` calls re-parses the MIME source each time rather than once - correct,
|
|
28
|
+
* not byte-perfect-efficient.
|
|
29
|
+
*/
|
|
30
|
+
export async function resolveMessageBodyBytes(target, messageRepo, blobStore) {
|
|
31
|
+
const uid = target.slice("message:".length);
|
|
32
|
+
const message = await messageRepo.findOne(uid, { ignoreACL: true });
|
|
33
|
+
if (!message) {
|
|
34
|
+
return Buffer.alloc(0);
|
|
35
|
+
}
|
|
36
|
+
const raw = await blobStore.get(message.bodyBlobKey);
|
|
37
|
+
const parsed = await simpleParser(raw);
|
|
38
|
+
const writer = new BufferWriter();
|
|
39
|
+
writer.writeNullTerminatedUtf16LE(parsed.text ?? "");
|
|
40
|
+
return writer.toBuffer();
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=MessageBodyStream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MessageBodyStream.js","sourceRoot":"","sources":["../../../src/rop/MessageBodyStream.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG1C,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD;;oEAEoE;AACpE,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC;AAEnC;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,MAAc,EAAE,WAA2B,EAAE,SAAoB;IAC3G,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAwB,MAAM,WAAW,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzF,IAAI,CAAC,OAAO,EAAE,CAAC;QACX,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC3B,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACrD,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;IAClC,MAAM,CAAC,0BAA0B,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IACrD,OAAO,MAAM,CAAC,QAAQ,EAAE,CAAC;AAC7B,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Degrades to empty-looking values for a `"message:<uid>"` target whose real `Message` has since vanished
|
|
2
|
+
* (soft-deleted or otherwise), the same "don't fail the whole ROP over one stale row" principle
|
|
3
|
+
* `FolderTarget.resolveFolderInfo` already applies. */
|
|
4
|
+
export async function resolveMessageInfo(target, messageRepo) {
|
|
5
|
+
const uid = target.slice("message:".length);
|
|
6
|
+
const message = await messageRepo.findOne(uid, { ignoreACL: true });
|
|
7
|
+
return {
|
|
8
|
+
subject: message?.subject ?? "",
|
|
9
|
+
read: message?.flags?.read ?? false,
|
|
10
|
+
hasAttachments: message?.hasAttachments ?? false,
|
|
11
|
+
receivedDate: message?.receivedDate ?? new Date(0),
|
|
12
|
+
labelUids: message?.labelUids ?? [],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Resolves the messages directly in `folderUid`, as `"message:<uid>"` target strings, for
|
|
16
|
+
* `RopGetContentsTable`. */
|
|
17
|
+
export async function resolveFolderMessages(folderUid, messageRepo) {
|
|
18
|
+
const messages = await messageRepo.find({ folderUid }, { ignoreACL: true });
|
|
19
|
+
return messages.map((m) => `message:${m.uid}`);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Returns `target`'s existing MID if an earlier `RopQueryRows` row already assigned one, otherwise assigns and
|
|
23
|
+
* remembers the next free small integer MID. The exact `FolderTarget.assignOrGetFid` pattern, adapted for
|
|
24
|
+
* messages: this is what lets a client `RopOpenMessage` a message it only ever learned about via a
|
|
25
|
+
* `RopQueryRows` row's `PidTagMid` column.
|
|
26
|
+
*
|
|
27
|
+
* Backed by `session.messageTargetIds` (target -> MID) and `session.nextMessageId`, an O(1) reverse index/
|
|
28
|
+
* counter pair rather than a linear scan of `session.messageIds` plus a `Math.max(...spread)` over its keys -
|
|
29
|
+
* see `FolderTarget.assignOrGetFid`'s own doc comment for why both of those were real costs (not just
|
|
30
|
+
* theoretical ones) at real mailbox/session scale: `messageIds` only ever grows for a session's lifetime as a
|
|
31
|
+
* client pages through a mailbox, so a linear-scan lookup repeated once per row is quadratic over a session
|
|
32
|
+
* that pages through many messages.
|
|
33
|
+
*/
|
|
34
|
+
export function assignOrGetMid(session, target) {
|
|
35
|
+
const existing = session.messageTargetIds[target];
|
|
36
|
+
if (existing !== undefined) {
|
|
37
|
+
return existing;
|
|
38
|
+
}
|
|
39
|
+
const mid = session.nextMessageId++;
|
|
40
|
+
session.messageIds[String(mid)] = target;
|
|
41
|
+
session.messageTargetIds[target] = mid;
|
|
42
|
+
return mid;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=MessageTarget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MessageTarget.js","sourceRoot":"","sources":["../../../src/rop/MessageTarget.ts"],"names":[],"mappings":"AAwBA;;uDAEuD;AACvD,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,MAAc,EAAE,WAA2B;IAChF,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAwB,MAAM,WAAW,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzF,OAAO;QACH,OAAO,EAAE,OAAO,EAAE,OAAO,IAAI,EAAE;QAC/B,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,IAAI,KAAK;QACnC,cAAc,EAAE,OAAO,EAAE,cAAc,IAAI,KAAK;QAChD,YAAY,EAAE,OAAO,EAAE,YAAY,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC;QAClD,SAAS,EAAE,OAAO,EAAE,SAAS,IAAI,EAAE;KACtC,CAAC;AACN,CAAC;AAED;4BAC4B;AAC5B,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,SAAiB,EAAE,WAA2B;IACtF,MAAM,QAAQ,GAAc,MAAM,WAAW,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvF,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc,CAAC,OAA2B,EAAE,MAAc;IACtE,MAAM,QAAQ,GAAG,OAAO,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAClD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,QAAQ,CAAC;IACpB,CAAC;IACD,MAAM,GAAG,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IACpC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC;IACzC,OAAO,CAAC,gBAAgB,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC;IACvC,OAAO,GAAG,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** The first numeric property ID this pragmatic subset ever assigns to a named property - real Exchange also
|
|
2
|
+
* reserves the `0x0000`-`0x7FFF` range for well-known/`PidTag*` properties, only ever assigning named
|
|
3
|
+
* properties IDs at `0x8000` and above (`[MS-OXCPRPT]`'s own `RopGetPropertyIdsFromNames` processing rules). */
|
|
4
|
+
const FIRST_NAMED_PROPERTY_ID = 0x8000;
|
|
5
|
+
/** A `PropertyName`'s registry key - a JSON string rather than a delimiter-joined one (e.g. `"<guid>:<lid>"`)
|
|
6
|
+
* because a string-`Kind` named property's own `name` is an arbitrary client-supplied string that could
|
|
7
|
+
* legitimately contain any delimiter this codec might otherwise pick (some real named properties do use
|
|
8
|
+
* URN-shaped names). `JSON.stringify` on a small, fixed-shape object has no such ambiguity. */
|
|
9
|
+
function keyFor(propertyName) {
|
|
10
|
+
return propertyName.kind === "lid"
|
|
11
|
+
? JSON.stringify({ guid: propertyName.guid.toLowerCase(), kind: "lid", lid: propertyName.lid })
|
|
12
|
+
: JSON.stringify({ guid: propertyName.guid.toLowerCase(), kind: "name", name: propertyName.name });
|
|
13
|
+
}
|
|
14
|
+
/** The highest numeric property ID a `PropertyTag`'s 16-bit `PropertyId` field can carry at all - not a
|
|
15
|
+
* pragmatic-subset choice, an absolute wire-format ceiling (`writeUInt16LE` cannot encode anything past this). */
|
|
16
|
+
const LAST_NAMED_PROPERTY_ID = 0xffff;
|
|
17
|
+
/**
|
|
18
|
+
* Returns `propertyName`'s existing numeric property ID if an earlier `RopGetPropertyIdsFromNames` call in
|
|
19
|
+
* this session already assigned one, otherwise assigns and remembers the next free ID starting at
|
|
20
|
+
* `FIRST_NAMED_PROPERTY_ID`. This pragmatic subset always behaves as if the request's own `Flags` field
|
|
21
|
+
* requested "assign a new ID if unmapped" (`0x02`) - real Exchange's alternative (`0x00`, "only return
|
|
22
|
+
* already-mapped IDs") exists to let a client probe without committing a mailbox-wide registration; since this
|
|
23
|
+
* registry is already only ever session-scoped (not a real persisted per-mailbox mapping table), there is no
|
|
24
|
+
* meaningful difference between "probe" and "assign" here.
|
|
25
|
+
*
|
|
26
|
+
* Backed by `session.namedProperties`/`namedPropertyIds` (a forward and reverse map, kept in sync) and
|
|
27
|
+
* `session.nextNamedPropertyId`, an O(1) lookup/assignment pair rather than a linear scan plus a
|
|
28
|
+
* `Math.max(...spread)` over `Object.values(session.namedProperties)` - both real costs at scale: the spread
|
|
29
|
+
* form risks a `RangeError: Maximum call stack size exceeded` once a session has registered enough distinct
|
|
30
|
+
* names to exceed V8's function-argument-count limit, and the registry only ever grows for a session's
|
|
31
|
+
* lifetime, so a linear scan repeated once per property per row is quadratic over a session that resolves many
|
|
32
|
+
* named properties.
|
|
33
|
+
*
|
|
34
|
+
* Once `nextNamedPropertyId` would exceed `LAST_NAMED_PROPERTY_ID` (32,768 distinct names already registered
|
|
35
|
+
* this session - the entire `0x8000`-`0xFFFF` numeric ID space this pragmatic subset has to hand out), a *new*
|
|
36
|
+
* name can no longer be assigned a real ID; this returns `0x0000` for it instead of throwing, the exact value
|
|
37
|
+
* `[MS-OXCPRPT]`'s own `RopGetPropertyIdsFromNames` processing rules already use for "this `PropertyName`
|
|
38
|
+
* could not be resolved" (the same value this pragmatic subset already produces for a `Kind = 0xFF` entry) -
|
|
39
|
+
* not a new failure mode, just the same one applied to a different unmappable case. A name already registered
|
|
40
|
+
* before the registry filled up keeps returning its real, previously-assigned ID.
|
|
41
|
+
*/
|
|
42
|
+
export function assignOrGetNamedPropertyId(session, propertyName) {
|
|
43
|
+
const key = keyFor(propertyName);
|
|
44
|
+
const existing = session.namedProperties[key];
|
|
45
|
+
if (existing !== undefined) {
|
|
46
|
+
return existing;
|
|
47
|
+
}
|
|
48
|
+
if (session.nextNamedPropertyId > LAST_NAMED_PROPERTY_ID) {
|
|
49
|
+
return 0x0000;
|
|
50
|
+
}
|
|
51
|
+
const id = session.nextNamedPropertyId++;
|
|
52
|
+
session.namedProperties[key] = id;
|
|
53
|
+
session.namedPropertyIds[id] = key;
|
|
54
|
+
return id;
|
|
55
|
+
}
|
|
56
|
+
/** The reverse lookup `RopSetProperties`/`RopGetPropertiesSpecific`/`RopQueryRows` use to recognize an incoming
|
|
57
|
+
* property ID `>= 0x8000` as one of this session's own mapped named properties - an O(1) lookup against
|
|
58
|
+
* `session.namedPropertyIds` (kept in sync by `assignOrGetNamedPropertyId`) rather than a linear scan of
|
|
59
|
+
* `session.namedProperties` with a `JSON.parse` per candidate - see that function's own doc comment for why
|
|
60
|
+
* this matters at scale, particularly since this is called once per named-property column per row from
|
|
61
|
+
* `RopQueryRows`. */
|
|
62
|
+
export function resolveNamedProperty(session, propertyId) {
|
|
63
|
+
const key = session.namedPropertyIds[propertyId];
|
|
64
|
+
return key !== undefined ? JSON.parse(key) : undefined;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=NamedPropertyRegistry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NamedPropertyRegistry.js","sourceRoot":"","sources":["../../../src/rop/NamedPropertyRegistry.ts"],"names":[],"mappings":"AAmBA;;gHAEgH;AAChH,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAEvC;;;+FAG+F;AAC/F,SAAS,MAAM,CAAC,YAA0B;IACtC,OAAO,YAAY,CAAC,IAAI,KAAK,KAAK;QAC9B,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,YAAY,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,YAAY,CAAC,GAAG,EAAE,CAAC;QAC/F,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,YAAY,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC;AAC3G,CAAC;AAED;kHACkH;AAClH,MAAM,sBAAsB,GAAG,MAAM,CAAC;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,0BAA0B,CAAC,OAA2B,EAAE,YAA0B;IAC9F,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;IACjC,MAAM,QAAQ,GAAG,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;IAC9C,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,QAAQ,CAAC;IACpB,CAAC;IACD,IAAI,OAAO,CAAC,mBAAmB,GAAG,sBAAsB,EAAE,CAAC;QACvD,OAAO,MAAM,CAAC;IAClB,CAAC;IACD,MAAM,EAAE,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC;IACzC,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;IAClC,OAAO,CAAC,gBAAgB,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC;IACnC,OAAO,EAAE,CAAC;AACd,CAAC;AAED;;;;;qBAKqB;AACrB,MAAM,UAAU,oBAAoB,CAAC,OAA2B,EAAE,UAAkB;IAChF,MAAM,GAAG,GAAG,OAAO,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC;IACjD,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAkB,CAAC,CAAC,CAAC,SAAS,CAAC;AAC7E,CAAC"}
|