@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,54 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import crypto from "crypto";
|
|
6
|
+
import { BufferWriter } from "../codec/BufferCursor.js";
|
|
7
|
+
import { encodeGuid } from "../codec/MapiGuid.js";
|
|
8
|
+
import { resolveCallerMailboxUid } from "@rapidmx/restapi";
|
|
9
|
+
/** `[MS-OXNSPI]` §2.2.1.2's own `MAPI_E_NOT_FOUND` error code, reused for "the caller has no mailbox in this
|
|
10
|
+
* deployment" - the one real failure mode `Bind` can hit in this pragmatic subset. */
|
|
11
|
+
const ERROR_NOT_FOUND = 0x8004010f;
|
|
12
|
+
/**
|
|
13
|
+
* `Bind`/`Unbind` request types (`[MS-OXCMAPIHTTP]` §2.2.5.1/§2.2.5.2, backed by `[MS-OXNSPI]`'s own `NspiBind`/
|
|
14
|
+
* `NspiUnbind` methods): establishes/tears down an NSPI Session Context, the address-book endpoint's own
|
|
15
|
+
* analog of EMSMDB's `Connect`/`Disconnect`.
|
|
16
|
+
*
|
|
17
|
+
* **No real session state is created or tracked** - a deliberate, documented simplification, not an oversight:
|
|
18
|
+
* every subsequent NSPI operation in this pragmatic subset (`GetMatches`) is independently `@Auth(["jwt"])`-
|
|
19
|
+
* protected and re-resolves the caller's own mailbox from that same JWT, exactly the way `RopLogon`/every ROP
|
|
20
|
+
* handler already does for EMSMDB - the real spec's `MapiContext`-equivalent session cookie exists mainly for
|
|
21
|
+
* multi-server-farm request affinity, not additional authentication, so this pragmatic subset mints an opaque,
|
|
22
|
+
* unvalidated cookie value purely for wire-format conformance and never checks it again. `HasState`/`State`
|
|
23
|
+
* (an optional `STAT` scoping the bind to one address-book container) is accepted but not decoded at all -
|
|
24
|
+
* this pragmatic subset has exactly one "container" (a mailbox's own `Contact` list) regardless.
|
|
25
|
+
*
|
|
26
|
+
* @author Jean-Philippe Steinmetz
|
|
27
|
+
*/
|
|
28
|
+
export async function handleNspiBind(res, user, mailboxRepo) {
|
|
29
|
+
const mailboxUid = await resolveCallerMailboxUid(mailboxRepo, user);
|
|
30
|
+
if (!mailboxUid) {
|
|
31
|
+
const body = new BufferWriter();
|
|
32
|
+
body.writeUInt32LE(ERROR_NOT_FOUND); // StatusCode - MUST NOT be 0 on failure
|
|
33
|
+
body.writeUInt32LE(0); // AuxiliaryBufferSize
|
|
34
|
+
res.status(200).send(body.toBuffer());
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
res.appendHeader("Set-Cookie", `NspiContext=${crypto.randomUUID()}`); // opaque, unvalidated - see class doc comment
|
|
38
|
+
const body = new BufferWriter();
|
|
39
|
+
body.writeUInt32LE(0); // StatusCode - success
|
|
40
|
+
body.writeUInt32LE(0); // ErrorCode - success
|
|
41
|
+
body.writeBytes(encodeGuid(crypto.randomUUID())); // ServerGuid - a fresh value per Bind is harmless, see class doc comment
|
|
42
|
+
body.writeUInt32LE(0); // AuxiliaryBufferSize
|
|
43
|
+
res.status(200).send(body.toBuffer());
|
|
44
|
+
}
|
|
45
|
+
/** `Unbind` never fails and has nothing to tear down - see `handleNspiBind`'s own doc comment for why no real
|
|
46
|
+
* session exists to release. */
|
|
47
|
+
export function handleNspiUnbind(res) {
|
|
48
|
+
const body = new BufferWriter();
|
|
49
|
+
body.writeUInt32LE(0); // StatusCode
|
|
50
|
+
body.writeUInt32LE(0); // ErrorCode
|
|
51
|
+
body.writeUInt32LE(0); // AuxiliaryBufferSize
|
|
52
|
+
res.status(200).send(body.toBuffer());
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=NspiBindHandler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NspiBindHandler.js","sourceRoot":"","sources":["../../../src/nspi/NspiBindHandler.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,MAAM,MAAM,QAAQ,CAAC;AAG5B,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAE3D;sFACsF;AACtF,MAAM,eAAe,GAAG,UAAU,CAAC;AAEnC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAiB,EAAE,IAAa,EAAE,WAA2B;IAC9F,MAAM,UAAU,GAAG,MAAM,uBAAuB,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;IACpE,IAAI,CAAC,UAAU,EAAE,CAAC;QACd,MAAM,IAAI,GAAG,IAAI,YAAY,EAAE,CAAC;QAChC,IAAI,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC,wCAAwC;QAC7E,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,sBAAsB;QAC7C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QACtC,OAAO;IACX,CAAC;IAED,GAAG,CAAC,YAAY,CAAC,YAAY,EAAE,eAAe,MAAM,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC,8CAA8C;IAEpH,MAAM,IAAI,GAAG,IAAI,YAAY,EAAE,CAAC;IAChC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,uBAAuB;IAC9C,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,sBAAsB;IAC7C,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC,yEAAyE;IAC3H,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,sBAAsB;IAC7C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;AAC1C,CAAC;AAED;gCACgC;AAChC,MAAM,UAAU,gBAAgB,CAAC,GAAiB;IAC9C,MAAM,IAAI,GAAG,IAAI,YAAY,EAAE,CAAC;IAChC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa;IACpC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY;IACnC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,sBAAsB;IAC7C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;AAC1C,CAAC"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { MAX_PROPERTY_TAG_COUNT, PropertyType, readPropertyTag, readPropertyTagArray, readTaggedPropertyValue, writePropertyTag, writePropertyValue, } from "../codec/PropertyValue.js";
|
|
2
|
+
export function readStat(reader) {
|
|
3
|
+
return {
|
|
4
|
+
sortType: reader.readUInt32LE(),
|
|
5
|
+
containerId: reader.readUInt32LE(),
|
|
6
|
+
currentRec: reader.readUInt32LE(),
|
|
7
|
+
delta: reader.readInt32LE(),
|
|
8
|
+
numPos: reader.readUInt32LE(),
|
|
9
|
+
totalRecs: reader.readUInt32LE(),
|
|
10
|
+
codePage: reader.readUInt32LE(),
|
|
11
|
+
templateLocale: reader.readUInt32LE(),
|
|
12
|
+
sortLocale: reader.readUInt32LE(),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function writeStat(writer, stat) {
|
|
16
|
+
writer.writeUInt32LE(stat.sortType);
|
|
17
|
+
writer.writeUInt32LE(stat.containerId);
|
|
18
|
+
writer.writeUInt32LE(stat.currentRec);
|
|
19
|
+
writer.writeInt32LE(stat.delta);
|
|
20
|
+
writer.writeUInt32LE(stat.numPos);
|
|
21
|
+
writer.writeUInt32LE(stat.totalRecs);
|
|
22
|
+
writer.writeUInt32LE(stat.codePage);
|
|
23
|
+
writer.writeUInt32LE(stat.templateLocale);
|
|
24
|
+
writer.writeUInt32LE(stat.sortLocale);
|
|
25
|
+
}
|
|
26
|
+
/** A blank `STAT` (`SortType`/`ContainerID`/... all zero) - what this pragmatic subset always echoes back
|
|
27
|
+
* (in `Bind`'s optional acknowledgement and `GetMatches`' response) regardless of the caller's own request
|
|
28
|
+
* `STAT`, since no real per-container/locale state is tracked. */
|
|
29
|
+
export const BLANK_STAT = {
|
|
30
|
+
sortType: 0,
|
|
31
|
+
containerId: 0,
|
|
32
|
+
currentRec: 0,
|
|
33
|
+
delta: 0,
|
|
34
|
+
numPos: 0,
|
|
35
|
+
totalRecs: 0,
|
|
36
|
+
codePage: 0,
|
|
37
|
+
templateLocale: 0,
|
|
38
|
+
sortLocale: 0,
|
|
39
|
+
};
|
|
40
|
+
/** `LargePropertyTagArray` (`[MS-OXCMAPIHTTP]` §2.2.1.8): a 4-byte count followed by that many `PropertyTag`
|
|
41
|
+
* structures (`[MS-OXCDATA]` §2.9 - the same `PropertyType`-then-`PropertyId` encoding `PropertyValue.ts`'s own
|
|
42
|
+
* `readPropertyTag`/`writePropertyTag` already implement). */
|
|
43
|
+
export function readLargePropertyTagArray(reader) {
|
|
44
|
+
const tags = readPropertyTagArray(reader, reader.readUInt32LE());
|
|
45
|
+
if (!tags) {
|
|
46
|
+
// Every requested column is resolved for every returned row, so the count is capped like EMSMDB's.
|
|
47
|
+
throw new RangeError(`NspiCodec: a LargePropertyTagArray may hold at most ${MAX_PROPERTY_TAG_COUNT} tags.`);
|
|
48
|
+
}
|
|
49
|
+
return tags;
|
|
50
|
+
}
|
|
51
|
+
export function writeLargePropertyTagArray(writer, tags) {
|
|
52
|
+
writer.writeUInt32LE(tags.length);
|
|
53
|
+
for (const tag of tags) {
|
|
54
|
+
writePropertyTag(writer, tag);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Writes one column's `AddressBookPropertyValue` (`[MS-OXCMAPIHTTP]` §2.2.1.1): a `PtypString`/`PtypString8`/
|
|
58
|
+
* `PtypBinary` value is preceded by a `HasValue` byte (always `0xFF`/`TRUE` in this pragmatic subset, which
|
|
59
|
+
* never reports a missing column value - the same "always emit a real value of the right type" principle
|
|
60
|
+
* `PropertyResolvers.defaultValueForType` already applies for EMSMDB); every other (fixed-size) type has no
|
|
61
|
+
* such prefix. `PtypMultiple*` columns (which need a `HasValue` byte per *element*, not just once) are never
|
|
62
|
+
* produced by this pragmatic subset's own fixed GAL column set and are intentionally not handled here. */
|
|
63
|
+
function writeAddressBookPropertyValue(writer, type, value) {
|
|
64
|
+
if (type === PropertyType.PtypString || type === PropertyType.PtypString8 || type === PropertyType.PtypBinary) {
|
|
65
|
+
writer.writeUInt8(0xff); // HasValue - always TRUE, see function doc comment
|
|
66
|
+
}
|
|
67
|
+
writePropertyValue(writer, type, value);
|
|
68
|
+
}
|
|
69
|
+
/** Writes one `AddressBookPropertyRow` (`[MS-OXCMAPIHTTP]` §2.2.1.7) - always `Flags=0x00` (every column
|
|
70
|
+
* value present without error), the exact `StandardPropertyRow`-equivalent choice `RopQueryRowsHandler`/
|
|
71
|
+
* `RopGetPropertiesSpecificHandler` already make for EMSMDB's own row encoding. */
|
|
72
|
+
export function writeAddressBookPropertyRow(writer, columns, values) {
|
|
73
|
+
writer.writeUInt8(0x00); // Flags - see function doc comment
|
|
74
|
+
columns.forEach((column, index) => writeAddressBookPropertyValue(writer, column.propertyType, values[index]));
|
|
75
|
+
}
|
|
76
|
+
/** Decodes a `Restriction` (`[MS-OXCDATA]` §2.12)'s leading `RestrictType` byte and, only for a
|
|
77
|
+
* `ContentRestriction`/`RES_CONTENT` (`0x03`) - the shape a real client's own GAL "search as you type" sends -
|
|
78
|
+
* extracts the plain string search term from its `TaggedValue` field.
|
|
79
|
+
*
|
|
80
|
+
* **Throws for any other restriction type** (`AND`/`OR`/`PropertyRestriction`/...) rather than degrading
|
|
81
|
+
* gracefully - unlike this pragmatic subset's usual "return a default instead of failing" stance elsewhere,
|
|
82
|
+
* a restriction's own byte length is type-dependent (12 different packet formats, several recursively nested),
|
|
83
|
+
* so once an unrecognized `RestrictType` is seen there is no safe way to know how many bytes to skip to reach
|
|
84
|
+
* the request body's own subsequent fields (`RowCount`/`Columns`/...) - silently returning `undefined` here
|
|
85
|
+
* would leave the reader mid-structure and corrupt every field decoded afterward. Throwing is the honest,
|
|
86
|
+
* safe choice for an input this narrow codec was never meant to parse, not a design gap.
|
|
87
|
+
*
|
|
88
|
+
* `FuzzyLevelLow`/`FuzzyLevelHigh` (exact/substring/prefix matching, case sensitivity) and the restriction's
|
|
89
|
+
* own target `PropertyTag` are decoded to advance the reader correctly but not honored - this pragmatic
|
|
90
|
+
* subset always does a case-insensitive substring match against its own fixed Contact field set regardless of
|
|
91
|
+
* which property or fuzzy level the client's restriction actually named. */
|
|
92
|
+
export function extractContentRestrictionSearchTerm(reader) {
|
|
93
|
+
const restrictType = reader.readUInt8();
|
|
94
|
+
if (restrictType !== 0x03) {
|
|
95
|
+
throw new Error(`NspiCodec: unsupported restriction type 0x${restrictType.toString(16)} (only ContentRestriction/RES_CONTENT is supported)`);
|
|
96
|
+
}
|
|
97
|
+
reader.readUInt16LE(); // FuzzyLevelLow - not honored, see function doc comment
|
|
98
|
+
reader.readUInt16LE(); // FuzzyLevelHigh - not honored, see function doc comment
|
|
99
|
+
readPropertyTag(reader); // PropertyTag (the restriction's own target column) - not honored
|
|
100
|
+
const tagged = readTaggedPropertyValue(reader);
|
|
101
|
+
if (typeof tagged.value !== "string") {
|
|
102
|
+
throw new Error("NspiCodec: ContentRestriction's TaggedValue was not a string.");
|
|
103
|
+
}
|
|
104
|
+
return tagged.value;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=NspiCodec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NspiCodec.js","sourceRoot":"","sources":["../../../src/nspi/NspiCodec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { boundedEscapedPattern } from "../RegexPatternUtils.js";
|
|
2
|
+
import { BufferReader, BufferWriter } from "../codec/BufferCursor.js";
|
|
3
|
+
import { PropertyType } from "../codec/PropertyValue.js";
|
|
4
|
+
import { defaultValueForType } from "../rop/PropertyResolvers.js";
|
|
5
|
+
import { BLANK_STAT, extractContentRestrictionSearchTerm, readLargePropertyTagArray, readStat, writeAddressBookPropertyRow, writeLargePropertyTagArray, writeStat, } from "./NspiCodec.js";
|
|
6
|
+
/** `PidTagDisplayName`/`PidTagEmailAddress` - the small default column set this pragmatic subset returns when
|
|
7
|
+
* a client's `GetMatches` request omits its own `Columns` field, mirroring EAS's own GAL result shape
|
|
8
|
+
* (`SearchCommand.ts`'s `DisplayName`/`EmailAddress`). */
|
|
9
|
+
const PID_TAG_DISPLAY_NAME = 0x3001;
|
|
10
|
+
const PID_TAG_EMAIL_ADDRESS = 0x3003;
|
|
11
|
+
/** The most rows one `GetMatches` lookup asks the repo for per query - `RepoUtils.find()`'s own page cap. */
|
|
12
|
+
export const MAX_MATCH_ROWS = 1000;
|
|
13
|
+
const DEFAULT_COLUMNS = [
|
|
14
|
+
{ propertyId: PID_TAG_DISPLAY_NAME, propertyType: PropertyType.PtypString },
|
|
15
|
+
{ propertyId: PID_TAG_EMAIL_ADDRESS, propertyType: PropertyType.PtypString },
|
|
16
|
+
];
|
|
17
|
+
/** A small, non-cryptographic FNV-1a 32-bit hash of a `Contact.uid`, used as that contact's `MinimalEntryID`
|
|
18
|
+
* in a `GetMatches` response. Deterministic per `uid` but not persisted or globally coordinated - acceptable
|
|
19
|
+
* here since no other operation in this pragmatic subset ever looks a `MinimalEntryID` back up (`NspiGetProps`/
|
|
20
|
+
* `NspiQueryRows`/... are out of scope), so nothing depends on it staying stable beyond a single response. */
|
|
21
|
+
function minimalEntryIdFor(uid) {
|
|
22
|
+
let hash = 0x811c9dc5;
|
|
23
|
+
for (let i = 0; i < uid.length; i++) {
|
|
24
|
+
hash ^= uid.charCodeAt(i);
|
|
25
|
+
hash = Math.imul(hash, 0x01000193);
|
|
26
|
+
}
|
|
27
|
+
return hash >>> 0;
|
|
28
|
+
}
|
|
29
|
+
function valueForColumn(contact, column) {
|
|
30
|
+
switch (column.propertyId) {
|
|
31
|
+
case PID_TAG_DISPLAY_NAME:
|
|
32
|
+
return contact.displayName;
|
|
33
|
+
case PID_TAG_EMAIL_ADDRESS:
|
|
34
|
+
// Optional-chained on `emails` itself, not just its first element - a Contact predating this field
|
|
35
|
+
// (or otherwise missing it) would throw on a bare `contact.emails[0]` before the `?.` on `.address`
|
|
36
|
+
// ever gets a chance to help.
|
|
37
|
+
return contact.emails?.[0]?.address ?? "";
|
|
38
|
+
default:
|
|
39
|
+
return defaultValueForType(column.propertyType);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Finds every `Contact` in `mailboxUid` matching `searchTerm` (a case-insensitive substring match against
|
|
43
|
+
* `displayName`/`givenName`/`surname`/`company`, queried per-field and merged in memory - the same two-backend
|
|
44
|
+
* workaround `SearchCommand.ts` already established for its own identical `$or`-is-Mongo-only /
|
|
45
|
+
* `ILike()`-needs-wrapping gaps) - or every `Contact` in the mailbox at all when `searchTerm` is `undefined`
|
|
46
|
+
* (no filter was supplied, or (`extractContentRestrictionSearchTerm`'s own contract) the filter was already
|
|
47
|
+
* known-good by the time this is called).
|
|
48
|
+
*
|
|
49
|
+
* Uses `RepoUtils`'s `regex(...)` operator (`@rapidrest/service-core` ^2.0), not `like(...)`: `like()` compiles
|
|
50
|
+
* a **glob** pattern (`*`/`?` as wildcards), which would require wrapping the term in `*...*` to get a substring
|
|
51
|
+
* match and would still treat a literal `*`/`?` in the search term itself as a wildcard (glob syntax has no
|
|
52
|
+
* escape for those two characters). `regex()` takes a real, already-anchoring-free regular expression compiled
|
|
53
|
+
* case-insensitively on both backends (`$regex`/`~*`/`REGEXP`), so escaping the term with `StringUtils
|
|
54
|
+
* .escapeRegExp` here gives a genuine literal-substring match with no residual wildcard ambiguity. `regex()`
|
|
55
|
+
* patterns are also independently validated by the framework itself (`ModelUtils.isUnsafeRegexPattern`) against
|
|
56
|
+
* catastrophic-backtracking shapes, but that guard is for patterns a caller assembles from parts; escaping the
|
|
57
|
+
* entire term with `escapeRegExp` before it ever reaches here already prevents any of those shapes from forming
|
|
58
|
+
* in the first place, since no unescaped metacharacter survives. */
|
|
59
|
+
async function findMatchingContacts(mailboxUid, contactRepo, searchTerm, limit) {
|
|
60
|
+
// `limit` goes in both the query (what the SQL query builder reads) and the options (what Mongo reads);
|
|
61
|
+
// without it each backend silently falls back to its own 100-row default regardless of `RowCount`.
|
|
62
|
+
if (!searchTerm) {
|
|
63
|
+
return contactRepo.find({ mailboxUid, limit }, { ignoreACL: true, limit });
|
|
64
|
+
}
|
|
65
|
+
// Bounded so the escaped operand never trips service-core's regex length guard, which would fail the whole
|
|
66
|
+
// NSPI call with INVALID_REQUEST for a long or metacharacter-heavy search term.
|
|
67
|
+
const pattern = boundedEscapedPattern(searchTerm);
|
|
68
|
+
const perField = await Promise.all(["displayName", "givenName", "surname", "company"].map((field) => contactRepo.find({ mailboxUid, [field]: `regex(${pattern})`, limit }, { ignoreACL: true, limit })));
|
|
69
|
+
const byUid = new Map();
|
|
70
|
+
for (const contact of perField.flat()) {
|
|
71
|
+
byUid.set(contact.uid, contact);
|
|
72
|
+
}
|
|
73
|
+
return Array.from(byUid.values()).sort((a, b) => a.displayName.localeCompare(b.displayName));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* `GetMatches` request type (`[MS-OXCMAPIHTTP]` §2.2.5.5, backed by `[MS-OXNSPI]`'s own `NspiGetMatches`
|
|
77
|
+
* method): searches the GAL (this library's `Contact` records, the same source of truth EAS's own `Search`
|
|
78
|
+
* command already uses) for entries matching the client's restriction, returning up to `RowCount` matches as
|
|
79
|
+
* `AddressBookPropertyRow`s. This is the real operation behind a client's own address-book "search as you
|
|
80
|
+
* type" UI - the closest real NSPI equivalent to this plan's own "minimal prefix/substring lookups" scope.
|
|
81
|
+
*
|
|
82
|
+
* `HasState`/`State` (an input `STAT` scoping the search and reporting position) and `HasMinimalIds`/
|
|
83
|
+
* `MinimalIds` (an "Explicit Table" restricting the search to a caller-supplied candidate set) are decoded to
|
|
84
|
+
* advance the reader correctly but not honored - this pragmatic subset always searches the caller's entire
|
|
85
|
+
* Contact list fresh on every call (no real per-container/table state, see `NspiBindHandler.ts`'s own doc
|
|
86
|
+
* comment for the same reasoning applied to `Bind`). `HasFilter`'s `Filter` is decoded via
|
|
87
|
+
* `NspiCodec.extractContentRestrictionSearchTerm` - see that function's own doc comment for why an
|
|
88
|
+
* unsupported restriction type throws rather than degrading. `HasPropertyName` (an alternate open-by-named-
|
|
89
|
+
* property addressing mode) is decoded to advance the reader correctly but never itself used to select a
|
|
90
|
+
* search field.
|
|
91
|
+
*
|
|
92
|
+
* The response's own `State` is always `BLANK_STAT` (with `TotalRecs` set to the real match count) - no real
|
|
93
|
+
* per-container `STAT` is tracked, matching this pragmatic subset's stated scope.
|
|
94
|
+
*
|
|
95
|
+
* @author Jean-Philippe Steinmetz
|
|
96
|
+
*/
|
|
97
|
+
export async function handleNspiGetMatches(req, res, mailboxUid, contactRepo) {
|
|
98
|
+
const reader = new BufferReader(req.rawBody ?? Buffer.alloc(0));
|
|
99
|
+
reader.readUInt32LE(); // Reserved
|
|
100
|
+
if (reader.readUInt8()) {
|
|
101
|
+
readStat(reader); // State - not honored, see function doc comment
|
|
102
|
+
}
|
|
103
|
+
if (reader.readUInt8()) {
|
|
104
|
+
// MinimalIds (an Explicit Table) - not honored, see function doc comment
|
|
105
|
+
// Skipped in one bounds-checked read rather than a loop over a client-supplied 32-bit count.
|
|
106
|
+
reader.readBytes(reader.readUInt32LE() * 4);
|
|
107
|
+
}
|
|
108
|
+
reader.readUInt32LE(); // InterfaceOptionFlags - reserved, always 0
|
|
109
|
+
const searchTerm = reader.readUInt8() ? extractContentRestrictionSearchTerm(reader) : undefined;
|
|
110
|
+
if (reader.readUInt8()) {
|
|
111
|
+
reader.readBytes(16); // PropertyNameGuid - not honored, see function doc comment
|
|
112
|
+
reader.readUInt32LE(); // PropertyNameId
|
|
113
|
+
}
|
|
114
|
+
const rowCount = reader.readUInt32LE();
|
|
115
|
+
const columns = reader.readUInt8() ? readLargePropertyTagArray(reader) : DEFAULT_COLUMNS;
|
|
116
|
+
// AuxiliaryBufferSize/AuxiliaryBuffer intentionally left unread - no auxiliary-payload support.
|
|
117
|
+
const matches = await findMatchingContacts(mailboxUid, contactRepo, searchTerm, Math.min(Math.max(rowCount, 1), MAX_MATCH_ROWS));
|
|
118
|
+
const page = matches.slice(0, rowCount);
|
|
119
|
+
const body = new BufferWriter();
|
|
120
|
+
body.writeUInt32LE(0); // StatusCode - success
|
|
121
|
+
body.writeUInt32LE(0); // ErrorCode - success
|
|
122
|
+
body.writeUInt8(1); // HasState
|
|
123
|
+
writeStat(body, { ...BLANK_STAT, totalRecs: matches.length });
|
|
124
|
+
body.writeUInt8(page.length > 0 ? 1 : 0); // HasMinimalIds
|
|
125
|
+
if (page.length > 0) {
|
|
126
|
+
body.writeUInt32LE(page.length);
|
|
127
|
+
for (const contact of page) {
|
|
128
|
+
body.writeUInt32LE(minimalEntryIdFor(contact.uid));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
body.writeUInt8(page.length > 0 ? 1 : 0); // HasColumnsAndRows
|
|
132
|
+
if (page.length > 0) {
|
|
133
|
+
writeLargePropertyTagArray(body, columns);
|
|
134
|
+
body.writeUInt32LE(page.length);
|
|
135
|
+
for (const contact of page) {
|
|
136
|
+
writeAddressBookPropertyRow(body, columns, columns.map((column) => valueForColumn(contact, column)));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
body.writeUInt32LE(0); // AuxiliaryBufferSize
|
|
140
|
+
res.status(200).send(body.toBuffer());
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=NspiGetMatchesHandler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NspiGetMatchesHandler.js","sourceRoot":"","sources":["../../../src/nspi/NspiGetMatchesHandler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import addressparser from "nodemailer/lib/addressparser/index.js";
|
|
6
|
+
import { literalQueryValue } from "./RestapiRules.js";
|
|
7
|
+
/** Splits a `PidTagDisplayTo`/`Cc`/`Bcc`-style string on the semicolons Outlook separates recipients with,
|
|
8
|
+
* trimming and dropping empty entries. Commas are not separators: a resolved recipient's display name commonly
|
|
9
|
+
* contains one (`Doe, Jane <jane@example.com>`). */
|
|
10
|
+
export function splitAddressList(value) {
|
|
11
|
+
if (!value) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
return value
|
|
15
|
+
.split(";")
|
|
16
|
+
.map((s) => s.trim())
|
|
17
|
+
.filter((s) => s.length > 0);
|
|
18
|
+
}
|
|
19
|
+
/** A bare `local@domain.tld` address: no whitespace (so no CR/LF), display name, angle brackets, quoting or
|
|
20
|
+
* separators. */
|
|
21
|
+
const PLAIN_EMAIL_ADDRESS = /^[^\s@<>()[\]",;:\\]+@[^\s@<>()[\]",;:\\]+\.[^\s@<>()[\]",;:\\.]+$/;
|
|
22
|
+
/** `true` for a plain SMTP address (see `PLAIN_EMAIL_ADDRESS`) no longer than RFC 5321's 254 characters. These
|
|
23
|
+
* strings reach MIME headers, the SMTP envelope and iCalendar `mailto:` lines, where a CR/LF would inject content. */
|
|
24
|
+
export function isPlainEmailAddress(value) {
|
|
25
|
+
return value.length <= 254 && PLAIN_EMAIL_ADDRESS.test(value);
|
|
26
|
+
}
|
|
27
|
+
/** Control characters (CR/LF included) are replaced in display names; nodemailer encodes the rest. */
|
|
28
|
+
function cleanName(name) {
|
|
29
|
+
return [...name].map((char) => (char.charCodeAt(0) < 0x20 || char.charCodeAt(0) === 0x7f ? " " : char)).join("").replace(/ +/g, " ").trim();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parses one `;`-separated entry with nodemailer's `addressparser`: `jane@example.com`, `Jane <jane@example.com>`,
|
|
33
|
+
* `"Doe, Jane" <jane@example.com>` or a bare display name `Jane Doe`. An entry holding several addresses yields one
|
|
34
|
+
* recipient each. Any address that isn't a plain SMTP address marks the entry invalid.
|
|
35
|
+
*/
|
|
36
|
+
export function parseRecipientEntry(entry) {
|
|
37
|
+
const parts = addressparser(entry, { flatten: true });
|
|
38
|
+
const addressed = parts.filter((part) => part.address);
|
|
39
|
+
if (addressed.length === 0) {
|
|
40
|
+
const name = cleanName(entry);
|
|
41
|
+
// Something address-shaped that addressparser couldn't take apart isn't a display name to look up.
|
|
42
|
+
return [/[@<>]/.test(name) ? { name, invalid: true } : { name }];
|
|
43
|
+
}
|
|
44
|
+
const nameOnlyParts = parts.filter((part) => !part.address && part.name).map((part) => part.name);
|
|
45
|
+
return addressed.map((part) => {
|
|
46
|
+
const name = cleanName(addressed.length === 1 ? [...nameOnlyParts, part.name].filter(Boolean).join(", ") : part.name);
|
|
47
|
+
const address = String(part.address);
|
|
48
|
+
return isPlainEmailAddress(address) && !/[\r\n]/.test(entry) ? { name, address } : { name, invalid: true };
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Every entry of a `PidTagDisplayTo`/`Cc`/`Bcc`-style string - see `parseRecipientEntry`. */
|
|
52
|
+
export function parseRecipientList(value) {
|
|
53
|
+
return splitAddressList(value).flatMap(parseRecipientEntry);
|
|
54
|
+
}
|
|
55
|
+
/** The plain addresses in a `PidTagDisplayTo`/`Cc`/`Bcc`-style string, including those written as
|
|
56
|
+
* `Name <address>`, skipping display names without an address and anything invalid. */
|
|
57
|
+
export function parseAddressList(value) {
|
|
58
|
+
return parseRecipientList(value)
|
|
59
|
+
.filter((entry) => entry.address !== undefined)
|
|
60
|
+
.map((entry) => entry.address);
|
|
61
|
+
}
|
|
62
|
+
/** The most contacts looked at when resolving one display name - two is enough to tell "exactly one" apart. */
|
|
63
|
+
const MAX_NAME_MATCHES = 2;
|
|
64
|
+
/** The most recipients (To, Cc and Bcc together) one submitted message or meeting may have - the same limit
|
|
65
|
+
* `@rapidmx/activesync`'s `ComposeMailCommand` enforces (`MAX_COMPOSE_RECIPIENTS`). */
|
|
66
|
+
export const MAX_RECIPIENTS_PER_MESSAGE = 500;
|
|
67
|
+
/** The caller's contacts whose display name is exactly `name`. The name comes from the client, so it is matched
|
|
68
|
+
* literally (`literalQueryValue`) and re-checked on the returned rows; a name the query layer rejects matches nothing.
|
|
69
|
+
* Charged to `budget` like every other single-query lookup (`resolveContentsKind`, `RopDeleteFolderHandler`'s own
|
|
70
|
+
* per-item delete) before the real DB round trip runs - a bare display name is exactly what a client crafting a
|
|
71
|
+
* malicious draft controls, and `RopSubmitMessageHandler` calls this once per unresolved To/Cc/Bcc entry (up to
|
|
72
|
+
* `MAX_RECIPIENTS_PER_MESSAGE` each), so without this charge a large recipient list would run unmetered queries. */
|
|
73
|
+
async function findContactsByDisplayName(context, name) {
|
|
74
|
+
if (!context.contactRepo) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
context.budget?.chargeQueries();
|
|
78
|
+
try {
|
|
79
|
+
const rows = await context.contactRepo.find({ mailboxUid: context.mailboxUid, displayName: literalQueryValue(name), limit: MAX_NAME_MATCHES }, { ignoreACL: true, limit: MAX_NAME_MATCHES });
|
|
80
|
+
return rows.filter((row) => row.displayName === name);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolves a display string into addresses. An entry carrying an address is used as is; a bare display name (Outlook
|
|
88
|
+
* writes names, not addresses, into `PidTagDisplayTo` once it has resolved a recipient against an address book) is
|
|
89
|
+
* looked up among the caller's own contacts by exact display name and used when exactly one contact matches, with
|
|
90
|
+
* that contact's first valid email address.
|
|
91
|
+
*/
|
|
92
|
+
export async function resolveRecipientList(value, context) {
|
|
93
|
+
const resolution = { recipients: [], unresolved: [], invalid: [] };
|
|
94
|
+
for (const entry of parseRecipientList(value)) {
|
|
95
|
+
if (entry.invalid) {
|
|
96
|
+
resolution.invalid.push(entry.name);
|
|
97
|
+
}
|
|
98
|
+
else if (entry.address) {
|
|
99
|
+
resolution.recipients.push({ name: entry.name, address: entry.address });
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const contacts = await findContactsByDisplayName(context, entry.name);
|
|
103
|
+
const address = contacts.length === 1 ? contacts[0].emails?.map((email) => email.address).find(isPlainEmailAddress) : undefined;
|
|
104
|
+
if (address) {
|
|
105
|
+
resolution.recipients.push({ name: entry.name, address });
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
resolution.unresolved.push(entry.name);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return resolution;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=AddressList.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AddressList.js","sourceRoot":"","sources":["../../../src/rop/AddressList.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { BusyStatus } from "@rapidmx/restapi";
|
|
2
|
+
import { findAllCapped, MAX_COLLECTION_ROWS } from "./RepoPaging.js";
|
|
3
|
+
/** Degrades to empty-looking values for a `"calendarEvent:<uid>"` target whose real `CalendarEvent` has since
|
|
4
|
+
* vanished (soft-deleted or otherwise) - the same "don't fail the whole ROP over one stale row" principle
|
|
5
|
+
* `MessageTarget.resolveMessageInfo`/`FolderTarget.resolveFolderInfo` already apply. */
|
|
6
|
+
export async function resolveCalendarEventInfo(target, calendarEventRepo) {
|
|
7
|
+
const uid = target.slice("calendarEvent:".length);
|
|
8
|
+
const event = await calendarEventRepo.findOne(uid, { ignoreACL: true });
|
|
9
|
+
return {
|
|
10
|
+
title: event?.title ?? "",
|
|
11
|
+
location: event?.location,
|
|
12
|
+
startDate: event?.startDate ?? new Date(0),
|
|
13
|
+
endDate: event?.endDate ?? new Date(0),
|
|
14
|
+
timezone: event?.timezone ?? "UTC",
|
|
15
|
+
busyStatus: event?.busyStatus ?? BusyStatus.BUSY,
|
|
16
|
+
recurrenceRule: event?.recurrenceRule,
|
|
17
|
+
reminderMinutesBeforeStart: event?.reminderMinutesBeforeStart,
|
|
18
|
+
organizerAddress: event?.organizer?.address ?? "",
|
|
19
|
+
attendees: event?.attendees ?? [],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** The order a calendar folder's events are listed in: latest start first, ties broken by `uid`. */
|
|
23
|
+
export const CALENDAR_EVENT_SORT = { startDate: "DESC", uid: "ASC" };
|
|
24
|
+
/** Resolves the calendar events directly in `folderUid`, latest first and capped at `MAX_COLLECTION_ROWS`, as
|
|
25
|
+
* `"calendarEvent:<uid>"` target strings. */
|
|
26
|
+
export async function resolveFolderCalendarEvents(folderUid, calendarEventRepo, budget) {
|
|
27
|
+
const { items: events } = await findAllCapped(calendarEventRepo, { folderUid }, CALENDAR_EVENT_SORT, MAX_COLLECTION_ROWS, budget);
|
|
28
|
+
return events.map((e) => `calendarEvent:${e.uid}`);
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=CalendarEventTarget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CalendarEventTarget.js","sourceRoot":"","sources":["../../../src/rop/CalendarEventTarget.ts"],"names":[],"mappings":"AAKA,OAAO,EAAY,UAAU,EAAiC,MAAM,kBAAkB,CAAC;AAEvF,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAiB,MAAM,iBAAiB,CAAC;AAuBpF;;wFAEwF;AACxF,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,MAAc,EAAE,iBAAiC;IAC5F,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAClD,MAAM,KAAK,GAA8B,MAAM,iBAAiB,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACnG,OAAO;QACH,KAAK,EAAE,KAAK,EAAE,KAAK,IAAI,EAAE;QACzB,QAAQ,EAAE,KAAK,EAAE,QAAQ;QACzB,SAAS,EAAE,KAAK,EAAE,SAAS,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC;QAC1C,OAAO,EAAE,KAAK,EAAE,OAAO,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC;QACtC,QAAQ,EAAE,KAAK,EAAE,QAAQ,IAAI,KAAK;QAClC,UAAU,EAAE,KAAK,EAAE,UAAU,IAAI,UAAU,CAAC,IAAI;QAChD,cAAc,EAAE,KAAK,EAAE,cAAc;QACrC,0BAA0B,EAAE,KAAK,EAAE,0BAA0B;QAC7D,gBAAgB,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,IAAI,EAAE;QACjD,SAAS,EAAE,KAAK,EAAE,SAAS,IAAI,EAAE;KACpC,CAAC;AACN,CAAC;AAED,oGAAoG;AACpG,MAAM,CAAC,MAAM,mBAAmB,GAAa,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;AAE/E;6CAC6C;AAC7C,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAAC,SAAiB,EAAE,iBAAiC,EAAE,MAAsB;IAC1H,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAgB,iBAAiB,EAAE,EAAE,SAAS,EAAE,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,CAAC,CAAC;IACjJ,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;AACvD,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { AttendeeResponseStatus, BusyStatus } from "@rapidmx/restapi";
|
|
6
|
+
/**
|
|
7
|
+
* The Calendar named-property identity table (`(PropertySet GUID, LID)` pairs) this pragmatic subset supports,
|
|
8
|
+
* per `[MS-OXPROPS]` - each pinned against its own MS-OXPROPS page (not invented), shared by `PropertyResolvers.ts`
|
|
9
|
+
* (the read side: `calendarEventValueFor`) and `RopSaveChangesMessageHandler.ts` (the write side: decoding a
|
|
10
|
+
* draft's accumulated `RopSetProperties` values back into a real `CalendarEvent`) so both directions of this
|
|
11
|
+
* mapping stay in exactly one place. See the architecture plan's "Calendar support" section for the full table.
|
|
12
|
+
*/
|
|
13
|
+
export const PSETID_APPOINTMENT = "00062002-0000-0000-c000-000000000046";
|
|
14
|
+
export const PSETID_COMMON = "00062008-0000-0000-c000-000000000046";
|
|
15
|
+
export const PSETID_MEETING = "6ed8da90-450b-101b-98da-00aa003f1305";
|
|
16
|
+
export const LID_LOCATION = 0x8208;
|
|
17
|
+
export const LID_APPOINTMENT_START_WHOLE = 0x820d;
|
|
18
|
+
export const LID_APPOINTMENT_END_WHOLE = 0x820e;
|
|
19
|
+
export const LID_BUSY_STATUS = 0x8205;
|
|
20
|
+
export const LID_RECURRING = 0x8223;
|
|
21
|
+
export const LID_REMINDER_SET = 0x8503;
|
|
22
|
+
export const LID_REMINDER_DELTA = 0x8501;
|
|
23
|
+
export const LID_RESPONSE_STATUS = 0x8218;
|
|
24
|
+
export const LID_APPOINTMENT_RECUR = 0x8216;
|
|
25
|
+
export const LID_TIME_ZONE_STRUCT = 0x8233;
|
|
26
|
+
/** `PidLidGlobalObjectId` (`PSETID_Meeting`) - see `GlobalObjectId.ts` for the BLOB it carries and
|
|
27
|
+
* `MeetingMessageClassHandler.ts` for how a meeting response's own copy of it is correlated back to the
|
|
28
|
+
* original `CalendarEvent`. */
|
|
29
|
+
export const LID_GLOBAL_OBJECT_ID = 0x00000003;
|
|
30
|
+
/** `PidLidBusyStatus`'s wire values (`[MS-OXOCAL]` §2.2.1.2, confirmed this session) - a different numbering
|
|
31
|
+
* than `CalendarSyncAdapter`'s own `BUSY_STATUS_CODES` table for the unrelated MS-ASCAL `BusyStatus` field.
|
|
32
|
+
* `olWorkingElsewhere` (0x00000004) has no equivalent in this library's own `BusyStatus` enum - never produced,
|
|
33
|
+
* and decoded (via `BUSY_STATUS_FROM_CODE`) with a fallback to `BUSY`, a documented, harmless approximation. */
|
|
34
|
+
export const BUSY_STATUS_CODES = {
|
|
35
|
+
[BusyStatus.FREE]: 0,
|
|
36
|
+
[BusyStatus.TENTATIVE]: 1,
|
|
37
|
+
[BusyStatus.BUSY]: 2,
|
|
38
|
+
[BusyStatus.OUT_OF_OFFICE]: 3,
|
|
39
|
+
};
|
|
40
|
+
export const BUSY_STATUS_FROM_CODE = {
|
|
41
|
+
0: BusyStatus.FREE,
|
|
42
|
+
1: BusyStatus.TENTATIVE,
|
|
43
|
+
2: BusyStatus.BUSY,
|
|
44
|
+
3: BusyStatus.OUT_OF_OFFICE,
|
|
45
|
+
};
|
|
46
|
+
/** `PidLidResponseStatus`'s wire values (`[MS-OXOCAL]` §2.2.1.11, confirmed this session) for the caller's own
|
|
47
|
+
* attendee record. `respOrganized` (0x00000001) is returned when the caller *is* the organizer - not a value
|
|
48
|
+
* `AttendeeResponseStatus` itself has a case for, since that enum only models an attendee's own response. */
|
|
49
|
+
export const RESPONSE_STATUS_CODES = {
|
|
50
|
+
[AttendeeResponseStatus.NEEDS_ACTION]: 5, // respNotResponded
|
|
51
|
+
[AttendeeResponseStatus.TENTATIVE]: 2, // respTentative
|
|
52
|
+
[AttendeeResponseStatus.ACCEPTED]: 3, // respAccepted
|
|
53
|
+
[AttendeeResponseStatus.DECLINED]: 4, // respDeclined
|
|
54
|
+
};
|
|
55
|
+
export const RESPONSE_STATUS_ORGANIZED = 1; // respOrganized
|
|
56
|
+
export const RESPONSE_STATUS_NONE = 0; // respNone
|
|
57
|
+
//# sourceMappingURL=CalendarNamedProperties.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CalendarNamedProperties.js","sourceRoot":"","sources":["../../../src/rop/CalendarNamedProperties.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEtE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,sCAAsC,CAAC;AACzE,MAAM,CAAC,MAAM,aAAa,GAAG,sCAAsC,CAAC;AACpE,MAAM,CAAC,MAAM,cAAc,GAAG,sCAAsC,CAAC;AAErE,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC;AACnC,MAAM,CAAC,MAAM,2BAA2B,GAAG,MAAM,CAAC;AAClD,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAM,CAAC;AAChD,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC;AACtC,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC;AACpC,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC;AACvC,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC;AACzC,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAC1C,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC;AAC5C,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAE3C;;+BAE+B;AAC/B,MAAM,CAAC,MAAM,oBAAoB,GAAG,UAAU,CAAC;AAE/C;;;gHAGgH;AAChH,MAAM,CAAC,MAAM,iBAAiB,GAA+B;IACzD,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;IACpB,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;IACzB,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;IACpB,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;CAChC,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAA+B;IAC7D,CAAC,EAAE,UAAU,CAAC,IAAI;IAClB,CAAC,EAAE,UAAU,CAAC,SAAS;IACvB,CAAC,EAAE,UAAU,CAAC,IAAI;IAClB,CAAC,EAAE,UAAU,CAAC,aAAa;CAC9B,CAAC;AAEF;;6GAE6G;AAC7G,MAAM,CAAC,MAAM,qBAAqB,GAA2C;IACzE,CAAC,sBAAsB,CAAC,YAAY,CAAC,EAAE,CAAC,EAAE,mBAAmB;IAC7D,CAAC,sBAAsB,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,gBAAgB;IACvD,CAAC,sBAAsB,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,eAAe;IACrD,CAAC,sBAAsB,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,eAAe;CACxD,CAAC;AACF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,CAAC,gBAAgB;AAC5D,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,CAAC,WAAW"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Degrades to empty-looking values for a `"contact:<uid>"` target whose real `Contact` has since vanished
|
|
2
|
+
* (soft-deleted or otherwise) - the same "don't fail the whole ROP over one stale row" principle
|
|
3
|
+
* `MessageTarget.resolveMessageInfo`/`CalendarEventTarget.resolveCalendarEventInfo` already apply. */
|
|
4
|
+
export async function resolveContactInfo(target, contactRepo) {
|
|
5
|
+
const uid = target.slice("contact:".length);
|
|
6
|
+
const contact = await contactRepo.findOne(uid, { ignoreACL: true });
|
|
7
|
+
return {
|
|
8
|
+
displayName: contact?.displayName ?? "",
|
|
9
|
+
givenName: contact?.givenName,
|
|
10
|
+
surname: contact?.surname,
|
|
11
|
+
email: contact?.emails?.[0]?.address,
|
|
12
|
+
businessPhone: contact?.phones?.[0]?.phoneNumber,
|
|
13
|
+
companyName: contact?.company,
|
|
14
|
+
jobTitle: contact?.jobTitle,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** The order a `CONTACTS` folder's contents table lists contacts in (see `ContentsTable.ts`). */
|
|
18
|
+
export const CONTACT_SORT = { displayName: "ASC", uid: "ASC" };
|
|
19
|
+
//# sourceMappingURL=ContactTarget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContactTarget.js","sourceRoot":"","sources":["../../../src/rop/ContactTarget.ts"],"names":[],"mappings":"AA8BA;;sGAEsG;AACtG,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,WAAW,EAAE,OAAO,EAAE,WAAW,IAAI,EAAE;QACvC,SAAS,EAAE,OAAO,EAAE,SAAS;QAC7B,OAAO,EAAE,OAAO,EAAE,OAAO;QACzB,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO;QACpC,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW;QAChD,WAAW,EAAE,OAAO,EAAE,OAAO;QAC7B,QAAQ,EAAE,OAAO,EAAE,QAAQ;KAC9B,CAAC;AACN,CAAC;AAED,iGAAiG;AACjG,MAAM,CAAC,MAAM,YAAY,GAAa,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC"}
|