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