@rapidmx/activesync 1.0.0-beta.0
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 +57 -0
- package/dist/lib/BaseDeviceSyncStateRoute.js +90 -0
- package/dist/lib/BaseDeviceSyncStateRoute.js.map +1 -0
- package/dist/lib/BaseEasRoute.js +213 -0
- package/dist/lib/BaseEasRoute.js.map +1 -0
- package/dist/lib/CompactDateTime.js +43 -0
- package/dist/lib/CompactDateTime.js.map +1 -0
- package/dist/lib/EasCommandHandler.js +2 -0
- package/dist/lib/EasCommandHandler.js.map +1 -0
- package/dist/lib/EasSyncKeyUtils.js +119 -0
- package/dist/lib/EasSyncKeyUtils.js.map +1 -0
- package/dist/lib/adapters/CalendarSyncAdapter.js +255 -0
- package/dist/lib/adapters/CalendarSyncAdapter.js.map +1 -0
- package/dist/lib/adapters/ContactsSyncAdapter.js +164 -0
- package/dist/lib/adapters/ContactsSyncAdapter.js.map +1 -0
- package/dist/lib/adapters/EasCollectionSyncAdapter.js +2 -0
- package/dist/lib/adapters/EasCollectionSyncAdapter.js.map +1 -0
- package/dist/lib/adapters/EmailSyncAdapter.js +230 -0
- package/dist/lib/adapters/EmailSyncAdapter.js.map +1 -0
- package/dist/lib/adapters/TasksSyncAdapter.js +100 -0
- package/dist/lib/adapters/TasksSyncAdapter.js.map +1 -0
- package/dist/lib/codec/WbxmlCodePages.js +706 -0
- package/dist/lib/codec/WbxmlCodePages.js.map +1 -0
- package/dist/lib/codec/WbxmlDecoder.js +154 -0
- package/dist/lib/codec/WbxmlDecoder.js.map +1 -0
- package/dist/lib/codec/WbxmlElement.js +31 -0
- package/dist/lib/codec/WbxmlElement.js.map +1 -0
- package/dist/lib/codec/WbxmlEncoder.js +88 -0
- package/dist/lib/codec/WbxmlEncoder.js.map +1 -0
- package/dist/lib/commands/ComposeMailCommand.js +187 -0
- package/dist/lib/commands/ComposeMailCommand.js.map +1 -0
- package/dist/lib/commands/FolderSyncCommand.js +150 -0
- package/dist/lib/commands/FolderSyncCommand.js.map +1 -0
- package/dist/lib/commands/GetItemEstimateCommand.js +149 -0
- package/dist/lib/commands/GetItemEstimateCommand.js.map +1 -0
- package/dist/lib/commands/ItemOperationsCommand.js +337 -0
- package/dist/lib/commands/ItemOperationsCommand.js.map +1 -0
- package/dist/lib/commands/MeetingResponseCommand.js +120 -0
- package/dist/lib/commands/MeetingResponseCommand.js.map +1 -0
- package/dist/lib/commands/MoveItemsCommand.js +103 -0
- package/dist/lib/commands/MoveItemsCommand.js.map +1 -0
- package/dist/lib/commands/PingCommand.js +145 -0
- package/dist/lib/commands/PingCommand.js.map +1 -0
- package/dist/lib/commands/ProvisionCommand.js +164 -0
- package/dist/lib/commands/ProvisionCommand.js.map +1 -0
- package/dist/lib/commands/ResolveRecipientsCommand.js +124 -0
- package/dist/lib/commands/ResolveRecipientsCommand.js.map +1 -0
- package/dist/lib/commands/SearchCommand.js +137 -0
- package/dist/lib/commands/SearchCommand.js.map +1 -0
- package/dist/lib/commands/SendMailCommand.js +18 -0
- package/dist/lib/commands/SendMailCommand.js.map +1 -0
- package/dist/lib/commands/SettingsCommand.js +138 -0
- package/dist/lib/commands/SettingsCommand.js.map +1 -0
- package/dist/lib/commands/SmartForwardCommand.js +22 -0
- package/dist/lib/commands/SmartForwardCommand.js.map +1 -0
- package/dist/lib/commands/SmartReplyCommand.js +22 -0
- package/dist/lib/commands/SmartReplyCommand.js.map +1 -0
- package/dist/lib/commands/SyncCommand.js +406 -0
- package/dist/lib/commands/SyncCommand.js.map +1 -0
- package/dist/lib/commands/mongo/FolderSyncCommandMongo.js +16 -0
- package/dist/lib/commands/mongo/FolderSyncCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/GetItemEstimateCommandMongo.js +21 -0
- package/dist/lib/commands/mongo/GetItemEstimateCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/ItemOperationsCommandMongo.js +18 -0
- package/dist/lib/commands/mongo/ItemOperationsCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/MeetingResponseCommandMongo.js +17 -0
- package/dist/lib/commands/mongo/MeetingResponseCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/MoveItemsCommandMongo.js +17 -0
- package/dist/lib/commands/mongo/MoveItemsCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/ResolveRecipientsCommandMongo.js +20 -0
- package/dist/lib/commands/mongo/ResolveRecipientsCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/SearchCommandMongo.js +20 -0
- package/dist/lib/commands/mongo/SearchCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/SendMailCommandMongo.js +17 -0
- package/dist/lib/commands/mongo/SendMailCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/SettingsCommandMongo.js +16 -0
- package/dist/lib/commands/mongo/SettingsCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/SmartForwardCommandMongo.js +17 -0
- package/dist/lib/commands/mongo/SmartForwardCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/SmartReplyCommandMongo.js +17 -0
- package/dist/lib/commands/mongo/SmartReplyCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/SyncCommandMongo.js +26 -0
- package/dist/lib/commands/mongo/SyncCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/index.js +14 -0
- package/dist/lib/commands/mongo/index.js.map +1 -0
- package/dist/lib/commands/sql/FolderSyncCommandSQL.js +16 -0
- package/dist/lib/commands/sql/FolderSyncCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/GetItemEstimateCommandSQL.js +21 -0
- package/dist/lib/commands/sql/GetItemEstimateCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/ItemOperationsCommandSQL.js +18 -0
- package/dist/lib/commands/sql/ItemOperationsCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/MeetingResponseCommandSQL.js +17 -0
- package/dist/lib/commands/sql/MeetingResponseCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/MoveItemsCommandSQL.js +17 -0
- package/dist/lib/commands/sql/MoveItemsCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/ResolveRecipientsCommandSQL.js +20 -0
- package/dist/lib/commands/sql/ResolveRecipientsCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/SearchCommandSQL.js +20 -0
- package/dist/lib/commands/sql/SearchCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/SendMailCommandSQL.js +17 -0
- package/dist/lib/commands/sql/SendMailCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/SettingsCommandSQL.js +16 -0
- package/dist/lib/commands/sql/SettingsCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/SmartForwardCommandSQL.js +17 -0
- package/dist/lib/commands/sql/SmartForwardCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/SmartReplyCommandSQL.js +17 -0
- package/dist/lib/commands/sql/SmartReplyCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/SyncCommandSQL.js +26 -0
- package/dist/lib/commands/sql/SyncCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/index.js +14 -0
- package/dist/lib/commands/sql/index.js.map +1 -0
- package/dist/lib/index.js +56 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/mongo/DeviceSyncStateRouteMongo.js +16 -0
- package/dist/lib/mongo/DeviceSyncStateRouteMongo.js.map +1 -0
- package/dist/lib/mongo/EasRouteMongo.js +51 -0
- package/dist/lib/mongo/EasRouteMongo.js.map +1 -0
- package/dist/lib/mongo/index.js +8 -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/sql/DeviceSyncStateRouteSQL.js +16 -0
- package/dist/lib/sql/DeviceSyncStateRouteSQL.js.map +1 -0
- package/dist/lib/sql/EasRouteSQL.js +50 -0
- package/dist/lib/sql/EasRouteSQL.js.map +1 -0
- package/dist/lib/sql/index.js +8 -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/BaseDeviceSyncStateRoute.d.ts +34 -0
- package/dist/types/BaseEasRoute.d.ts +64 -0
- package/dist/types/CompactDateTime.d.ts +22 -0
- package/dist/types/EasCommandHandler.d.ts +59 -0
- package/dist/types/EasSyncKeyUtils.d.ts +89 -0
- package/dist/types/adapters/CalendarSyncAdapter.d.ts +52 -0
- package/dist/types/adapters/ContactsSyncAdapter.d.ts +33 -0
- package/dist/types/adapters/EasCollectionSyncAdapter.d.ts +52 -0
- package/dist/types/adapters/EmailSyncAdapter.d.ts +53 -0
- package/dist/types/adapters/TasksSyncAdapter.d.ts +32 -0
- package/dist/types/codec/WbxmlCodePages.d.ts +45 -0
- package/dist/types/codec/WbxmlDecoder.d.ts +33 -0
- package/dist/types/codec/WbxmlElement.d.ts +43 -0
- package/dist/types/codec/WbxmlEncoder.d.ts +20 -0
- package/dist/types/commands/ComposeMailCommand.d.ts +50 -0
- package/dist/types/commands/FolderSyncCommand.d.ts +25 -0
- package/dist/types/commands/GetItemEstimateCommand.d.ts +43 -0
- package/dist/types/commands/ItemOperationsCommand.d.ts +72 -0
- package/dist/types/commands/MeetingResponseCommand.d.ts +32 -0
- package/dist/types/commands/MoveItemsCommand.d.ts +29 -0
- package/dist/types/commands/PingCommand.d.ts +33 -0
- package/dist/types/commands/ProvisionCommand.d.ts +54 -0
- package/dist/types/commands/ResolveRecipientsCommand.d.ts +34 -0
- package/dist/types/commands/SearchCommand.d.ts +39 -0
- package/dist/types/commands/SendMailCommand.d.ts +10 -0
- package/dist/types/commands/SettingsCommand.d.ts +37 -0
- package/dist/types/commands/SmartForwardCommand.d.ts +17 -0
- package/dist/types/commands/SmartReplyCommand.d.ts +17 -0
- package/dist/types/commands/SyncCommand.d.ts +100 -0
- package/dist/types/commands/mongo/FolderSyncCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/GetItemEstimateCommandMongo.d.ts +7 -0
- package/dist/types/commands/mongo/ItemOperationsCommandMongo.d.ts +9 -0
- package/dist/types/commands/mongo/MeetingResponseCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/MoveItemsCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/ResolveRecipientsCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/SearchCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/SendMailCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/SettingsCommandMongo.d.ts +7 -0
- package/dist/types/commands/mongo/SmartForwardCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/SmartReplyCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/SyncCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/index.d.ts +9 -0
- package/dist/types/commands/sql/FolderSyncCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/GetItemEstimateCommandSQL.d.ts +7 -0
- package/dist/types/commands/sql/ItemOperationsCommandSQL.d.ts +9 -0
- package/dist/types/commands/sql/MeetingResponseCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/MoveItemsCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/ResolveRecipientsCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/SearchCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/SendMailCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/SettingsCommandSQL.d.ts +7 -0
- package/dist/types/commands/sql/SmartForwardCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/SmartReplyCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/SyncCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/index.d.ts +9 -0
- package/dist/types/index.d.ts +51 -0
- package/dist/types/mongo/DeviceSyncStateRouteMongo.d.ts +8 -0
- package/dist/types/mongo/EasRouteMongo.d.ts +14 -0
- package/dist/types/mongo/index.d.ts +3 -0
- package/dist/types/mongo.d.ts +1 -0
- package/dist/types/sql/DeviceSyncStateRouteSQL.d.ts +8 -0
- package/dist/types/sql/EasRouteSQL.d.ts +13 -0
- package/dist/types/sql/index.d.ts +3 -0
- package/dist/types/sql.d.ts +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
11
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
12
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
13
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
14
|
+
import { createClient } from "redis";
|
|
15
|
+
import { ObjectDecorators } from "@rapidrest/core";
|
|
16
|
+
import { ACLAction, ACLUtils } from "@rapidrest/service-core";
|
|
17
|
+
import { WbxmlCodePage } from "../codec/WbxmlCodePages.js";
|
|
18
|
+
import { childText, element, findChild, findChildren, textElement } from "../codec/WbxmlElement.js";
|
|
19
|
+
const { Config, Inject } = ObjectDecorators;
|
|
20
|
+
/** MS-ASCMD `Ping` `Status` codes this pragmatic subset distinguishes - not the full enumeration the real
|
|
21
|
+
* spec defines (e.g. it also has codes for "too many folders", "folder hierarchy changed", etc.), matching
|
|
22
|
+
* this library's "pragmatic subset, not full fidelity" precedent elsewhere. */
|
|
23
|
+
const STATUS_NO_CHANGES = "1";
|
|
24
|
+
const STATUS_CHANGES_FOUND = "2";
|
|
25
|
+
const STATUS_MISSING_PARAMETERS = "3";
|
|
26
|
+
/**
|
|
27
|
+
* Handles EAS `Ping`: a long-poll HTTP request that blocks for up to `HeartbeatInterval` seconds waiting for a
|
|
28
|
+
* change in any of the client-specified folders, then reports which (if any) actually changed. No websocket
|
|
29
|
+
* needed - subscribes a plain, request-scoped Redis client (the same construction `BasePushRoute.connect()`
|
|
30
|
+
* uses, minus its own per-socket bookkeeping this one-shot request doesn't need) directly to the requested
|
|
31
|
+
* `folderUid` channels. Every `Message`/`CalendarEvent`/etc. mutation already publishes to exactly these
|
|
32
|
+
* channels via `BaseScopedChildRoute.notify()`/`RepoUtils`'s own push - `Ping` needs no new publish call site
|
|
33
|
+
* of its own, only a subscriber.
|
|
34
|
+
*
|
|
35
|
+
* Requested folder uids are filtered down to only those the caller currently has `READ` on before subscribing -
|
|
36
|
+
* matching every other command's ownership check on a client-supplied id, since without it a device could
|
|
37
|
+
* long-poll indefinitely on any folder uid it happens to know (including one it used to have legitimate access
|
|
38
|
+
* to and no longer does) and learn purely from this channel when that other mailbox's data changes.
|
|
39
|
+
*
|
|
40
|
+
* @author Jean-Philippe Steinmetz
|
|
41
|
+
*/
|
|
42
|
+
export class PingCommand {
|
|
43
|
+
constructor() {
|
|
44
|
+
this.command = "Ping";
|
|
45
|
+
this.minHeartbeatSeconds = 60;
|
|
46
|
+
this.maxHeartbeatSeconds = 1740;
|
|
47
|
+
}
|
|
48
|
+
async handle(ctx) {
|
|
49
|
+
if (!ctx.request) {
|
|
50
|
+
return this.statusResponse(STATUS_MISSING_PARAMETERS);
|
|
51
|
+
}
|
|
52
|
+
const foldersEl = findChild(ctx.request, "Folders");
|
|
53
|
+
const requestedFolderUids = foldersEl
|
|
54
|
+
? findChildren(foldersEl, "Folder")
|
|
55
|
+
.map((folderEl) => childText(folderEl, "ServerId"))
|
|
56
|
+
.filter((uid) => !!uid)
|
|
57
|
+
: [];
|
|
58
|
+
if (requestedFolderUids.length === 0) {
|
|
59
|
+
return this.statusResponse(STATUS_MISSING_PARAMETERS);
|
|
60
|
+
}
|
|
61
|
+
// Every other command checks ownership before acting on a client-supplied folder id - `Ping` must too,
|
|
62
|
+
// or a device that once had a folder shared with it (and so learned its real uid) could keep watching
|
|
63
|
+
// that folder's live activity via this long-poll indefinitely, even after the share is later revoked.
|
|
64
|
+
// Filtered down to the permitted subset rather than failing the whole request: `Ping`'s wire response
|
|
65
|
+
// has no per-folder status to report a partial denial through, and a client legitimately re-sends the
|
|
66
|
+
// same folder list across requests regardless of access changes it has no way to know about yet.
|
|
67
|
+
const permitted = await Promise.all(requestedFolderUids.map((uid) => this.aclUtils.hasPermission(ctx.user, uid, ACLAction.READ)));
|
|
68
|
+
const folderUids = requestedFolderUids.filter((_uid, i) => permitted[i]);
|
|
69
|
+
if (folderUids.length === 0) {
|
|
70
|
+
return this.statusResponse(STATUS_MISSING_PARAMETERS);
|
|
71
|
+
}
|
|
72
|
+
const requestedSeconds = Number(childText(ctx.request, "HeartbeatInterval") ?? this.minHeartbeatSeconds);
|
|
73
|
+
const heartbeatSeconds = Math.min(Math.max(Number.isFinite(requestedSeconds) ? requestedSeconds : this.minHeartbeatSeconds, this.minHeartbeatSeconds), this.maxHeartbeatSeconds);
|
|
74
|
+
const changedFolderUids = await this.waitForChange(folderUids, heartbeatSeconds);
|
|
75
|
+
if (changedFolderUids.length === 0) {
|
|
76
|
+
return this.statusResponse(STATUS_NO_CHANGES);
|
|
77
|
+
}
|
|
78
|
+
return element(WbxmlCodePage.Ping, "Ping", [
|
|
79
|
+
textElement(WbxmlCodePage.Ping, "Status", STATUS_CHANGES_FOUND),
|
|
80
|
+
element(WbxmlCodePage.Ping, "Folders", changedFolderUids.map((uid) => textElement(WbxmlCodePage.Ping, "Folder", uid))),
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
statusResponse(status) {
|
|
84
|
+
return element(WbxmlCodePage.Ping, "Ping", [textElement(WbxmlCodePage.Ping, "Status", status)]);
|
|
85
|
+
}
|
|
86
|
+
/** Subscribes to `folderUids` and resolves with whichever of them published a change first, or `[]` if
|
|
87
|
+
* `timeoutSeconds` elapses with no publish. A missing `datastores:events` config (Redis pub/sub not set up
|
|
88
|
+
* for this deployment) fails open to "no changes" rather than blocking forever or throwing - `Ping` is a
|
|
89
|
+
* best-effort low-latency notification path, not the only way a client ever discovers new mail (it will
|
|
90
|
+
* eventually poll a real `Sync`/`FolderSync` regardless). */
|
|
91
|
+
async waitForChange(folderUids, timeoutSeconds) {
|
|
92
|
+
if (!this.redisConfig) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
const client = createClient({ url: this.redisConfig.url });
|
|
96
|
+
await client.connect();
|
|
97
|
+
try {
|
|
98
|
+
return await new Promise((resolve) => {
|
|
99
|
+
let settled = false;
|
|
100
|
+
const timer = setTimeout(() => {
|
|
101
|
+
if (!settled) {
|
|
102
|
+
settled = true;
|
|
103
|
+
resolve([]);
|
|
104
|
+
}
|
|
105
|
+
}, timeoutSeconds * 1000);
|
|
106
|
+
client
|
|
107
|
+
.subscribe(folderUids, (_message, channel) => {
|
|
108
|
+
if (!settled) {
|
|
109
|
+
settled = true;
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
resolve([channel]);
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
.catch(() => {
|
|
115
|
+
if (!settled) {
|
|
116
|
+
settled = true;
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
resolve([]);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
await client.unsubscribe(folderUids).catch(() => undefined);
|
|
125
|
+
await client.disconnect().catch(() => undefined);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
__decorate([
|
|
130
|
+
Config("datastores:events", null),
|
|
131
|
+
__metadata("design:type", Object)
|
|
132
|
+
], PingCommand.prototype, "redisConfig", void 0);
|
|
133
|
+
__decorate([
|
|
134
|
+
Config("mail:eas:ping_min_heartbeat_seconds", 60),
|
|
135
|
+
__metadata("design:type", Number)
|
|
136
|
+
], PingCommand.prototype, "minHeartbeatSeconds", void 0);
|
|
137
|
+
__decorate([
|
|
138
|
+
Config("mail:eas:ping_max_heartbeat_seconds", 1740),
|
|
139
|
+
__metadata("design:type", Number)
|
|
140
|
+
], PingCommand.prototype, "maxHeartbeatSeconds", void 0);
|
|
141
|
+
__decorate([
|
|
142
|
+
Inject(ACLUtils),
|
|
143
|
+
__metadata("design:type", ACLUtils)
|
|
144
|
+
], PingCommand.prototype, "aclUtils", void 0);
|
|
145
|
+
//# sourceMappingURL=PingCommand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PingCommand.js","sourceRoot":"","sources":["../../../src/commands/PingCommand.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
11
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
12
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
13
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
14
|
+
import * as crypto from "crypto";
|
|
15
|
+
import { ObjectDecorators } from "@rapidrest/core";
|
|
16
|
+
import { WbxmlCodePage } from "../codec/WbxmlCodePages.js";
|
|
17
|
+
import { childText, element, findChild, textElement } from "../codec/WbxmlElement.js";
|
|
18
|
+
import { persistDeviceSyncState } from "../EasSyncKeyUtils.js";
|
|
19
|
+
const { Config } = ObjectDecorators;
|
|
20
|
+
const DEFAULT_POLICY_TYPE = "MS-EAS-Provisioning-WBXML";
|
|
21
|
+
/**
|
|
22
|
+
* Handles the two-request EAS `Provision` handshake (MS-ASPROV) every client must complete before any other
|
|
23
|
+
* command is honored (see `BaseEasRoute`'s provisioning gate), plus the three-step `RemoteWipe` sub-flow that
|
|
24
|
+
* rides the same command.
|
|
25
|
+
*
|
|
26
|
+
* **Policy issuance/enforcement**: the password/encryption requirements sent back in request 1's policy
|
|
27
|
+
* document are sourced from `@Config("mail:eas:provision:*")` (defaults below are permissive but not
|
|
28
|
+
* `0`/`false` across the board, unlike the old hardcoded document) - a deployment can tighten them without
|
|
29
|
+
* code changes. Enforcement itself is honest but shallow: request 2 must carry back `Status: 1` on its own
|
|
30
|
+
* `Policy` (a device that reports it could *not* apply the policy, or omits `Status` entirely, is rejected
|
|
31
|
+
* without provisioning) - this library does not itself verify the device's actual password/encryption state
|
|
32
|
+
* beyond trusting that self-reported status, matching MS-ASPROV's own protocol design (the wire protocol has
|
|
33
|
+
* no way for the server to inspect device state directly either).
|
|
34
|
+
*
|
|
35
|
+
* - **Request 1** (no `PolicyKey` in the body): mint a new policy key, store it on `DeviceSyncState` (not yet
|
|
36
|
+
* provisioned), and send back the policy document under that key. If a `RemoteWipe` was requested for this
|
|
37
|
+
* device (`DeviceSyncState.remoteWipeRequested`, set by an admin - see the remote-wipe route), skip normal
|
|
38
|
+
* policy issuance entirely and send the `RemoteWipe` directive instead.
|
|
39
|
+
* - **Request 2** (client echoes the `PolicyKey` back, acknowledging the policy): if the key matches what was
|
|
40
|
+
* minted in request 1 *and* the client's own `Status` is `1`, mark the device provisioned and re-confirm the
|
|
41
|
+
* same key; anything else (a stale/replayed key, a device that never actually saw request 1's response, or a
|
|
42
|
+
* device reporting it could not comply) is rejected without provisioning.
|
|
43
|
+
* - **RemoteWipe acknowledgement**: after wiping itself, a device sends a bare `<Provision><RemoteWipe>
|
|
44
|
+
* <Status>1</Status></RemoteWipe></Provision>` (no `Policies`). Detected first, ahead of the normal
|
|
45
|
+
* issue/acknowledge branching. Clears `remoteWipeRequested` and stamps `remoteWipeAcknowledgedAt` for audit,
|
|
46
|
+
* but deliberately leaves `provisioned` untouched (`false`, from when the wipe was requested) - the device
|
|
47
|
+
* must complete a genuine fresh Provision handshake to re-add the account, it does not fall straight back into
|
|
48
|
+
* "provisioned". `remoteWipeAccountOnly` is recorded for admin audit only; the wire directive sent to the
|
|
49
|
+
* device is identical either way (a real "wipe just this account's data" vs. "wipe the whole device"
|
|
50
|
+
* distinction would require an MDM-capable client extension this library doesn't implement).
|
|
51
|
+
*
|
|
52
|
+
* @author Jean-Philippe Steinmetz
|
|
53
|
+
*/
|
|
54
|
+
export class ProvisionCommand {
|
|
55
|
+
constructor() {
|
|
56
|
+
this.command = "Provision";
|
|
57
|
+
this.passwordEnabled = true;
|
|
58
|
+
this.minPasswordLength = 4;
|
|
59
|
+
this.maxFailedAttempts = 8;
|
|
60
|
+
this.requireDeviceEncryption = true;
|
|
61
|
+
this.allowSimplePassword = false;
|
|
62
|
+
}
|
|
63
|
+
async handle(ctx) {
|
|
64
|
+
if (ctx.request && findChild(ctx.request, "RemoteWipe")) {
|
|
65
|
+
return await this.acknowledgeRemoteWipe(ctx);
|
|
66
|
+
}
|
|
67
|
+
const policiesEl = ctx.request ? findChild(ctx.request, "Policies") : undefined;
|
|
68
|
+
const policyEl = policiesEl ? findChild(policiesEl, "Policy") : undefined;
|
|
69
|
+
const policyType = (policyEl ? childText(policyEl, "PolicyType") : undefined) ?? DEFAULT_POLICY_TYPE;
|
|
70
|
+
const clientPolicyKey = policyEl ? childText(policyEl, "PolicyKey") : undefined;
|
|
71
|
+
const clientStatus = policyEl ? childText(policyEl, "Status") : undefined;
|
|
72
|
+
if (!clientPolicyKey) {
|
|
73
|
+
return await this.issuePolicy(ctx, policyType);
|
|
74
|
+
}
|
|
75
|
+
return await this.acknowledgePolicy(ctx, policyType, clientPolicyKey, clientStatus);
|
|
76
|
+
}
|
|
77
|
+
/** Request 1: mint and store a new policy key, send the policy document - or, if a remote wipe is
|
|
78
|
+
* pending for this device, the `RemoteWipe` directive instead. */
|
|
79
|
+
async issuePolicy(ctx, policyType) {
|
|
80
|
+
if (ctx.deviceSyncState.remoteWipeRequested) {
|
|
81
|
+
return element(WbxmlCodePage.Provision, "Provision", [
|
|
82
|
+
textElement(WbxmlCodePage.Provision, "Status", "1"),
|
|
83
|
+
element(WbxmlCodePage.Provision, "RemoteWipe", [textElement(WbxmlCodePage.Provision, "Status", "1")]),
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
const policyKey = crypto.randomBytes(8).toString("hex");
|
|
87
|
+
await this.persist(ctx, { policyKey, provisioned: false });
|
|
88
|
+
return element(WbxmlCodePage.Provision, "Provision", [
|
|
89
|
+
textElement(WbxmlCodePage.Provision, "Status", "1"),
|
|
90
|
+
element(WbxmlCodePage.Provision, "Policies", [
|
|
91
|
+
element(WbxmlCodePage.Provision, "Policy", [
|
|
92
|
+
textElement(WbxmlCodePage.Provision, "PolicyType", policyType),
|
|
93
|
+
textElement(WbxmlCodePage.Provision, "Status", "1"),
|
|
94
|
+
textElement(WbxmlCodePage.Provision, "PolicyKey", policyKey),
|
|
95
|
+
element(WbxmlCodePage.Provision, "Data", [
|
|
96
|
+
element(WbxmlCodePage.Provision, "EASProvisionDoc", [
|
|
97
|
+
textElement(WbxmlCodePage.Provision, "DevicePasswordEnabled", this.passwordEnabled ? "1" : "0"),
|
|
98
|
+
textElement(WbxmlCodePage.Provision, "MinDevicePasswordLength", String(this.minPasswordLength)),
|
|
99
|
+
textElement(WbxmlCodePage.Provision, "MaxDevicePasswordFailedAttempts", String(this.maxFailedAttempts)),
|
|
100
|
+
textElement(WbxmlCodePage.Provision, "AllowSimpleDevicePassword", this.allowSimplePassword ? "1" : "0"),
|
|
101
|
+
textElement(WbxmlCodePage.Provision, "RequireDeviceEncryption", this.requireDeviceEncryption ? "1" : "0"),
|
|
102
|
+
textElement(WbxmlCodePage.Provision, "AttachmentsEnabled", "1"),
|
|
103
|
+
]),
|
|
104
|
+
]),
|
|
105
|
+
]),
|
|
106
|
+
]),
|
|
107
|
+
]);
|
|
108
|
+
}
|
|
109
|
+
/** Request 2: the client acknowledges the policy key it was handed in request 1, self-reporting whether
|
|
110
|
+
* it actually applied the policy via its own `Status`. */
|
|
111
|
+
async acknowledgePolicy(ctx, policyType, clientPolicyKey, clientStatus) {
|
|
112
|
+
if (clientPolicyKey !== ctx.deviceSyncState.policyKey || clientStatus !== "1") {
|
|
113
|
+
// Status 2 ("protocol error" per MS-ASPROV) - an approximation, not a byte-exact enumeration of
|
|
114
|
+
// every real status code MS-ASPROV defines; this pragmatic subset only distinguishes success from
|
|
115
|
+
// "something is wrong, start over" (see this class's own doc comment on scope), and deliberately
|
|
116
|
+
// does not distinguish a key mismatch from a device self-reporting non-compliance.
|
|
117
|
+
return element(WbxmlCodePage.Provision, "Provision", [textElement(WbxmlCodePage.Provision, "Status", "2")]);
|
|
118
|
+
}
|
|
119
|
+
await this.persist(ctx, { policyKey: clientPolicyKey, provisioned: true });
|
|
120
|
+
return element(WbxmlCodePage.Provision, "Provision", [
|
|
121
|
+
textElement(WbxmlCodePage.Provision, "Status", "1"),
|
|
122
|
+
element(WbxmlCodePage.Provision, "Policies", [
|
|
123
|
+
element(WbxmlCodePage.Provision, "Policy", [
|
|
124
|
+
textElement(WbxmlCodePage.Provision, "PolicyType", policyType),
|
|
125
|
+
textElement(WbxmlCodePage.Provision, "Status", "1"),
|
|
126
|
+
textElement(WbxmlCodePage.Provision, "PolicyKey", clientPolicyKey),
|
|
127
|
+
]),
|
|
128
|
+
]),
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
131
|
+
/** The device has wiped itself and is acknowledging - clear the pending flag but leave `provisioned`
|
|
132
|
+
* alone (still `false`, from when the wipe was requested) so a genuine re-provision is required. */
|
|
133
|
+
async acknowledgeRemoteWipe(ctx) {
|
|
134
|
+
await persistDeviceSyncState(ctx.deviceSyncState, ctx.deviceSyncStateRepo, {
|
|
135
|
+
remoteWipeRequested: false,
|
|
136
|
+
remoteWipeAcknowledgedAt: new Date(),
|
|
137
|
+
});
|
|
138
|
+
return element(WbxmlCodePage.Provision, "Provision", [textElement(WbxmlCodePage.Provision, "Status", "1")]);
|
|
139
|
+
}
|
|
140
|
+
async persist(ctx, changes) {
|
|
141
|
+
await persistDeviceSyncState(ctx.deviceSyncState, ctx.deviceSyncStateRepo, changes);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
__decorate([
|
|
145
|
+
Config("mail:eas:provision:password_enabled", true),
|
|
146
|
+
__metadata("design:type", Boolean)
|
|
147
|
+
], ProvisionCommand.prototype, "passwordEnabled", void 0);
|
|
148
|
+
__decorate([
|
|
149
|
+
Config("mail:eas:provision:min_password_length", 4),
|
|
150
|
+
__metadata("design:type", Number)
|
|
151
|
+
], ProvisionCommand.prototype, "minPasswordLength", void 0);
|
|
152
|
+
__decorate([
|
|
153
|
+
Config("mail:eas:provision:max_failed_attempts", 8),
|
|
154
|
+
__metadata("design:type", Number)
|
|
155
|
+
], ProvisionCommand.prototype, "maxFailedAttempts", void 0);
|
|
156
|
+
__decorate([
|
|
157
|
+
Config("mail:eas:provision:require_device_encryption", true),
|
|
158
|
+
__metadata("design:type", Boolean)
|
|
159
|
+
], ProvisionCommand.prototype, "requireDeviceEncryption", void 0);
|
|
160
|
+
__decorate([
|
|
161
|
+
Config("mail:eas:provision:allow_simple_password", false),
|
|
162
|
+
__metadata("design:type", Boolean)
|
|
163
|
+
], ProvisionCommand.prototype, "allowSimplePassword", void 0);
|
|
164
|
+
//# sourceMappingURL=ProvisionCommand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProvisionCommand.js","sourceRoot":"","sources":["../../../src/commands/ProvisionCommand.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
11
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
12
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
13
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
14
|
+
import { ObjectDecorators } from "@rapidrest/core";
|
|
15
|
+
import { RepoUtils } from "@rapidrest/service-core";
|
|
16
|
+
import { WbxmlCodePage } from "../codec/WbxmlCodePages.js";
|
|
17
|
+
import { element, findChildren, textElement } from "../codec/WbxmlElement.js";
|
|
18
|
+
const { Config, Init } = ObjectDecorators;
|
|
19
|
+
/** A simple, deliberately permissive email-address shape check - just enough to distinguish "the client sent
|
|
20
|
+
* an address it already knows how to reach" from "the client sent a partial name/string that needs GAL
|
|
21
|
+
* lookup", not full RFC 5322 validation. */
|
|
22
|
+
const LOOKS_LIKE_EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
23
|
+
/** [MS-ASCMD] `ResolveRecipients` per-`To` `Status` codes this pragmatic subset actually distinguishes: `1`
|
|
24
|
+
* success (at least one match, or the input already looked like a resolvable address), `4` no match found -
|
|
25
|
+
* an approximation, not a byte-exact enumeration of every real status code MS-ASCMD's `ResolveRecipients` page
|
|
26
|
+
* defines, matching `ProvisionCommand`'s own identical precedent for the same reasoning. */
|
|
27
|
+
const STATUS_SUCCESS = "1";
|
|
28
|
+
const STATUS_NOT_FOUND = "4";
|
|
29
|
+
/** Escapes regex metacharacters so a client-supplied query string is matched literally - identical helper to
|
|
30
|
+
* `SearchCommand`'s own (small enough, and specific enough to each command's own surrounding logic, that
|
|
31
|
+
* extracting a shared utility for two call sites isn't worth a new cross-command dependency). */
|
|
32
|
+
function escapeForLikeQuery(value) {
|
|
33
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Handles EAS `ResolveRecipients`: resolves each `<To>` value (a display name, partial name, or address) the
|
|
37
|
+
* client is unsure how to reach against the mailbox's own `Contact` (GAL) store - the same substring-match
|
|
38
|
+
* approach `SearchCommand` uses for its own `Store Name="GAL"` lookups, duplicated rather than shared (see
|
|
39
|
+
* `escapeForLikeQuery`'s own doc comment).
|
|
40
|
+
*
|
|
41
|
+
* **Pragmatic subset**: no free-busy `Availability`, no S/MIME `Certificates`/`Options` handling at all - this
|
|
42
|
+
* command's real-world use is overwhelmingly enterprise S/MIME certificate lookup, which this library doesn't
|
|
43
|
+
* implement elsewhere either (`ComposeMailCommand` sends plain MIME, never signed/encrypted). A `<To>` value
|
|
44
|
+
* that already looks like an email address (`LOOKS_LIKE_EMAIL`) is echoed straight back as its own single
|
|
45
|
+
* exact match rather than searched for, matching a real client's own common case of resolving an address it
|
|
46
|
+
* already typed correctly.
|
|
47
|
+
*
|
|
48
|
+
* `contactClass` is supplied by the Mongo/SQL concrete subclasses.
|
|
49
|
+
*
|
|
50
|
+
* @author Jean-Philippe Steinmetz
|
|
51
|
+
*/
|
|
52
|
+
export class ResolveRecipientsCommand {
|
|
53
|
+
constructor() {
|
|
54
|
+
this.command = "ResolveRecipients";
|
|
55
|
+
this.maxMatches = 10;
|
|
56
|
+
}
|
|
57
|
+
async init() {
|
|
58
|
+
this.contactRepo = await this._objectFactory.newInstance(RepoUtils, {
|
|
59
|
+
name: this.contactClass.name,
|
|
60
|
+
args: [this.contactClass],
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async handle(ctx) {
|
|
64
|
+
const toEls = ctx.request ? findChildren(ctx.request, "To") : [];
|
|
65
|
+
const responses = [];
|
|
66
|
+
for (const toEl of toEls) {
|
|
67
|
+
const value = toEl.text ?? "";
|
|
68
|
+
responses.push(await this.resolveOne(ctx, value));
|
|
69
|
+
}
|
|
70
|
+
return element(WbxmlCodePage.ResolveRecipients, "ResolveRecipients", [
|
|
71
|
+
textElement(WbxmlCodePage.ResolveRecipients, "Status", "1"),
|
|
72
|
+
...responses,
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
async resolveOne(ctx, value) {
|
|
76
|
+
if (LOOKS_LIKE_EMAIL.test(value)) {
|
|
77
|
+
return this.responseElement(value, STATUS_SUCCESS, [this.recipientElement(value, undefined)]);
|
|
78
|
+
}
|
|
79
|
+
const pattern = this.likePattern(escapeForLikeQuery(value));
|
|
80
|
+
const findOptions = { ignoreACL: true, limit: this.maxMatches };
|
|
81
|
+
const perField = await Promise.all(["displayName", "givenName", "surname"].map((field) => this.contactRepo.find({ mailboxUid: ctx.mailboxUid, [field]: `like(${pattern})` }, findOptions)));
|
|
82
|
+
const byUid = new Map();
|
|
83
|
+
for (const contact of perField.flat()) {
|
|
84
|
+
byUid.set(contact.uid, contact);
|
|
85
|
+
}
|
|
86
|
+
const matches = Array.from(byUid.values())
|
|
87
|
+
.filter((contact) => contact.emails[0])
|
|
88
|
+
.slice(0, this.maxMatches);
|
|
89
|
+
if (matches.length === 0) {
|
|
90
|
+
return this.responseElement(value, STATUS_NOT_FOUND, []);
|
|
91
|
+
}
|
|
92
|
+
return this.responseElement(value, STATUS_SUCCESS, matches.map((contact) => this.recipientElement(contact.emails[0].address, contact.displayName)));
|
|
93
|
+
}
|
|
94
|
+
recipientElement(address, displayName) {
|
|
95
|
+
return element(WbxmlCodePage.ResolveRecipients, "Recipient", [
|
|
96
|
+
textElement(WbxmlCodePage.ResolveRecipients, "Type", "1"),
|
|
97
|
+
...(displayName ? [textElement(WbxmlCodePage.ResolveRecipients, "DisplayName", displayName)] : []),
|
|
98
|
+
textElement(WbxmlCodePage.ResolveRecipients, "EmailAddress", address),
|
|
99
|
+
]);
|
|
100
|
+
}
|
|
101
|
+
responseElement(to, status, recipients) {
|
|
102
|
+
return element(WbxmlCodePage.ResolveRecipients, "Response", [
|
|
103
|
+
textElement(WbxmlCodePage.ResolveRecipients, "To", to),
|
|
104
|
+
textElement(WbxmlCodePage.ResolveRecipients, "Status", status),
|
|
105
|
+
...(recipients.length > 0
|
|
106
|
+
? [
|
|
107
|
+
textElement(WbxmlCodePage.ResolveRecipients, "RecipientCount", String(recipients.length)),
|
|
108
|
+
...recipients,
|
|
109
|
+
]
|
|
110
|
+
: []),
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
__decorate([
|
|
115
|
+
Config("mail:eas:resolve_recipients_max_matches", 10),
|
|
116
|
+
__metadata("design:type", Number)
|
|
117
|
+
], ResolveRecipientsCommand.prototype, "maxMatches", void 0);
|
|
118
|
+
__decorate([
|
|
119
|
+
Init,
|
|
120
|
+
__metadata("design:type", Function),
|
|
121
|
+
__metadata("design:paramtypes", []),
|
|
122
|
+
__metadata("design:returntype", Promise)
|
|
123
|
+
], ResolveRecipientsCommand.prototype, "init", null);
|
|
124
|
+
//# sourceMappingURL=ResolveRecipientsCommand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResolveRecipientsCommand.js","sourceRoot":"","sources":["../../../src/commands/ResolveRecipientsCommand.ts"],"names":[],"mappings":";;;;;;;;;AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAiB,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEnE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAqB,MAAM,0BAA0B,CAAC;AAEjG,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,gBAAgB,CAAC;AAE1C;;4CAE4C;AAC5C,MAAM,gBAAgB,GAAG,4BAA4B,CAAC;AAEtD;;;4FAG4F;AAC5F,MAAM,cAAc,GAAG,GAAG,CAAC;AAC3B,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B;;iGAEiG;AACjG,SAAS,kBAAkB,CAAC,KAAa;IACrC,OAAO,KAAK,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,OAAgB,wBAAwB;IAA9C;QACoB,YAAO,GAAG,mBAAmB,CAAC;QAStC,eAAU,GAAW,EAAE,CAAC;IA+EpC,CAAC;IAvEgB,AAAN,KAAK,CAAC,IAAI;QACb,IAAI,CAAC,WAAW,GAAG,MAAM,IAAI,CAAC,cAAe,CAAC,WAAW,CAAC,SAAS,EAAE;YACjE,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,IAAI;YAC5B,IAAI,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC;SAC5B,CAAC,CAAC;IACP,CAAC;IAEM,KAAK,CAAC,MAAM,CAAC,GAAsB;QACtC,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACjE,MAAM,SAAS,GAAmB,EAAE,CAAC;QACrC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;YAC9B,SAAS,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;QACtD,CAAC;QACD,OAAO,OAAO,CAAC,aAAa,CAAC,iBAAiB,EAAE,mBAAmB,EAAE;YACjE,WAAW,CAAC,aAAa,CAAC,iBAAiB,EAAE,QAAQ,EAAE,GAAG,CAAC;YAC3D,GAAG,SAAS;SACf,CAAC,CAAC;IACP,CAAC;IAEO,KAAK,CAAC,UAAU,CAAC,GAAsB,EAAE,KAAa;QAC1D,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC/B,OAAO,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,cAAc,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;QAClG,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5D,MAAM,WAAW,GAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC;QACrE,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,CAC9B,CAAC,aAAa,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAClD,IAAI,CAAC,WAAY,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,EAAE,QAAQ,OAAO,GAAG,EAAS,EAAE,WAAW,CAAC,CAC1G,CACJ,CAAC;QACF,MAAM,KAAK,GAAG,IAAI,GAAG,EAAqC,CAAC;QAC3D,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;YACpC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACpC,CAAC;QACD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;aACrC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;aACtC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QAE/B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,OAAO,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,gBAAgB,EAAE,EAAE,CAAC,CAAC;QAC7D,CAAC;QAED,OAAO,IAAI,CAAC,eAAe,CACvB,KAAK,EACL,cAAc,EACd,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC,CAClG,CAAC;IACN,CAAC;IAEO,gBAAgB,CAAC,OAAe,EAAE,WAA+B;QACrE,OAAO,OAAO,CAAC,aAAa,CAAC,iBAAiB,EAAE,WAAW,EAAE;YACzD,WAAW,CAAC,aAAa,CAAC,iBAAiB,EAAE,MAAM,EAAE,GAAG,CAAC;YACzD,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,aAAa,CAAC,iBAAiB,EAAE,aAAa,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAClG,WAAW,CAAC,aAAa,CAAC,iBAAiB,EAAE,cAAc,EAAE,OAAO,CAAC;SACxE,CAAC,CAAC;IACP,CAAC;IAEO,eAAe,CAAC,EAAU,EAAE,MAAc,EAAE,UAA0B;QAC1E,OAAO,OAAO,CAAC,aAAa,CAAC,iBAAiB,EAAE,UAAU,EAAE;YACxD,WAAW,CAAC,aAAa,CAAC,iBAAiB,EAAE,IAAI,EAAE,EAAE,CAAC;YACtD,WAAW,CAAC,aAAa,CAAC,iBAAiB,EAAE,QAAQ,EAAE,MAAM,CAAC;YAC9D,GAAG,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC;gBACrB,CAAC,CAAC;oBACI,WAAW,CAAC,aAAa,CAAC,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;oBACzF,GAAG,UAAU;iBAChB;gBACH,CAAC,CAAC,EAAE,CAAC;SACZ,CAAC,CAAC;IACP,CAAC;CACJ;AA/EW;IADP,MAAM,CAAC,yCAAyC,EAAE,EAAE,CAAC;;4DACtB;AAQnB;IADZ,IAAI;;;;oDAMJ"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
11
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
12
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
13
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
14
|
+
import { ApiError, ObjectDecorators } from "@rapidrest/core";
|
|
15
|
+
import { ApiErrorMessages, ApiErrors, RepoUtils } from "@rapidrest/service-core";
|
|
16
|
+
import { WbxmlCodePage } from "../codec/WbxmlCodePages.js";
|
|
17
|
+
import { childText, element, findChild, textElement } from "../codec/WbxmlElement.js";
|
|
18
|
+
const { Config, Init } = ObjectDecorators;
|
|
19
|
+
/** Escapes regex metacharacters so a client-supplied search string is matched literally by `RepoUtils`' own
|
|
20
|
+
* `like(pattern)` query operator (which compiles to a case-insensitive `$regex`) - without this, a query
|
|
21
|
+
* containing e.g. `.` or `(` would be interpreted as regex syntax rather than the literal characters typed. */
|
|
22
|
+
function escapeForLikeQuery(value) {
|
|
23
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24
|
+
}
|
|
25
|
+
/** Parses a `Range` value (`"m-n"`, a zero-based inclusive index pair) into `{ start, end }`, falling back to
|
|
26
|
+
* `defaultEnd` for a missing/malformed value - never trusting the client to request more than `maxEnd` rows. */
|
|
27
|
+
function parseRange(raw, defaultEnd, maxEnd) {
|
|
28
|
+
const match = raw?.match(/^(\d+)-(\d+)$/);
|
|
29
|
+
if (!match) {
|
|
30
|
+
return { start: 0, end: Math.min(defaultEnd, maxEnd) };
|
|
31
|
+
}
|
|
32
|
+
const start = Number(match[1]);
|
|
33
|
+
const end = Math.min(Number(match[2]), maxEnd);
|
|
34
|
+
return start <= end ? { start, end } : { start: 0, end: Math.min(defaultEnd, maxEnd) };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Handles EAS `Search` for the `GAL` store only - this library's `Contact` records are also the source of
|
|
38
|
+
* truth for GAL lookups against a mailbox's own address book (see the architecture note on `Contact` itself);
|
|
39
|
+
* searching a mailbox's item contents or a document library (the other two `Search` store types the real spec
|
|
40
|
+
* defines) is out of scope for this pragmatic subset.
|
|
41
|
+
*
|
|
42
|
+
* Per the plan's own scope: a simple case-insensitive substring match via `RepoUtils.find()` directly, not the
|
|
43
|
+
* heavier `SearchProvider` full-text index - GAL lookups are small-scale exact/prefix matching against a
|
|
44
|
+
* personal address book, not relevance-ranked full text over large content. Only `displayName`/`givenName`/
|
|
45
|
+
* `surname`/`company` are matched - `Contact.emails`/`phones` are embedded arrays of objects, which a plain
|
|
46
|
+
* per-field regex query can't reach into on either backend (confirmed: MongoDB's `$regex` against an
|
|
47
|
+
* array-of-objects field matches nothing useful, and this library's own query-injection guard rejects
|
|
48
|
+
* dot-notation field paths like `"emails.address"` outright) - a documented gap, not an oversight.
|
|
49
|
+
*
|
|
50
|
+
* `contactClass` is supplied by the Mongo/SQL concrete subclasses.
|
|
51
|
+
*
|
|
52
|
+
* @author Jean-Philippe Steinmetz
|
|
53
|
+
*/
|
|
54
|
+
export class SearchCommand {
|
|
55
|
+
constructor() {
|
|
56
|
+
this.command = "Search";
|
|
57
|
+
this.defaultRangeEnd = 9;
|
|
58
|
+
this.maxRangeEnd = 99;
|
|
59
|
+
}
|
|
60
|
+
async init() {
|
|
61
|
+
this.contactRepo = await this._objectFactory.newInstance(RepoUtils, {
|
|
62
|
+
name: this.contactClass.name,
|
|
63
|
+
args: [this.contactClass],
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async handle(ctx) {
|
|
67
|
+
if (!this.contactRepo) {
|
|
68
|
+
throw new ApiError(ApiErrors.INTERNAL_ERROR, 500, ApiErrorMessages.INTERNAL_ERROR);
|
|
69
|
+
}
|
|
70
|
+
const storeEl = ctx.request ? findChild(ctx.request, "Store") : undefined;
|
|
71
|
+
const name = storeEl ? childText(storeEl, "Name") : undefined;
|
|
72
|
+
const query = storeEl ? childText(storeEl, "Query") : undefined;
|
|
73
|
+
if (!storeEl || name !== "GAL" || !query) {
|
|
74
|
+
throw new ApiError(ApiErrors.INVALID_REQUEST, 400, "Search requires a Store with Name 'GAL' and a Query.");
|
|
75
|
+
}
|
|
76
|
+
const optionsEl = findChild(storeEl, "Options");
|
|
77
|
+
const { start, end } = parseRange(optionsEl ? childText(optionsEl, "Range") : undefined, this.defaultRangeEnd, this.maxRangeEnd);
|
|
78
|
+
// `$or` is a Mongo-only feature of this framework's query builder - `buildSearchQuerySQL` (confirmed by
|
|
79
|
+
// reading its source) has no handling for it at all, so passing one on the SQL backend silently builds
|
|
80
|
+
// a broken TypeORM `where` clause (a literal `$or` property, not a real OR) and 500s. Querying each
|
|
81
|
+
// field separately and merging in memory - the same workaround `EasSyncKeyUtils.computeChanges()`
|
|
82
|
+
// already uses for its own two-backend query gap - works identically on both backends instead.
|
|
83
|
+
const pattern = this.likePattern(escapeForLikeQuery(query));
|
|
84
|
+
const findOptions = { ignoreACL: true, limit: this.maxRangeEnd + 1 };
|
|
85
|
+
const perField = await Promise.all(["displayName", "givenName", "surname", "company"].map((field) => this.contactRepo.find({ mailboxUid: ctx.mailboxUid, [field]: `like(${pattern})`, limit: this.maxRangeEnd + 1 }, findOptions)));
|
|
86
|
+
const byUid = new Map();
|
|
87
|
+
for (const contact of perField.flat()) {
|
|
88
|
+
byUid.set((contact).uid, contact);
|
|
89
|
+
}
|
|
90
|
+
const matches = Array.from(byUid.values()).sort((a, b) => a.displayName.localeCompare(b.displayName));
|
|
91
|
+
const page = matches.slice(start, end + 1);
|
|
92
|
+
// Both ends forced to 0 when there are no matches at all - `start` alone (from a client-requested
|
|
93
|
+
// Range like "5-10") would otherwise survive unclamped, producing a malformed "5-0" (start > end) once
|
|
94
|
+
// `end` collapses to 0. A non-empty result always has a real, in-range `start` already (see
|
|
95
|
+
// `parseRange`), so this only ever changes behavior for the zero-match case.
|
|
96
|
+
const rangeStart = matches.length === 0 ? 0 : start;
|
|
97
|
+
const rangeEnd = matches.length === 0 ? 0 : Math.min(end, matches.length - 1);
|
|
98
|
+
return element(WbxmlCodePage.Search, "Search", [
|
|
99
|
+
textElement(WbxmlCodePage.Search, "Status", "1"),
|
|
100
|
+
element(WbxmlCodePage.Search, "Response", [
|
|
101
|
+
element(WbxmlCodePage.Search, "Store", [
|
|
102
|
+
textElement(WbxmlCodePage.Search, "Status", "1"),
|
|
103
|
+
...page.map((contact) => this.contactToResult(contact)),
|
|
104
|
+
textElement(WbxmlCodePage.Search, "Range", `${rangeStart}-${rangeEnd}`),
|
|
105
|
+
textElement(WbxmlCodePage.Search, "Total", String(matches.length)),
|
|
106
|
+
]),
|
|
107
|
+
]),
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
contactToResult(contact) {
|
|
111
|
+
const properties = [
|
|
112
|
+
textElement(WbxmlCodePage.Gal, "DisplayName", contact.displayName),
|
|
113
|
+
...(contact.givenName ? [textElement(WbxmlCodePage.Gal, "FirstName", contact.givenName)] : []),
|
|
114
|
+
...(contact.surname ? [textElement(WbxmlCodePage.Gal, "LastName", contact.surname)] : []),
|
|
115
|
+
...(contact.company ? [textElement(WbxmlCodePage.Gal, "Company", contact.company)] : []),
|
|
116
|
+
...(contact.jobTitle ? [textElement(WbxmlCodePage.Gal, "Title", contact.jobTitle)] : []),
|
|
117
|
+
...(contact.emails[0] ? [textElement(WbxmlCodePage.Gal, "EmailAddress", contact.emails[0].address)] : []),
|
|
118
|
+
...(contact.phones[0] ? [textElement(WbxmlCodePage.Gal, "Phone", contact.phones[0].phoneNumber)] : []),
|
|
119
|
+
];
|
|
120
|
+
return element(WbxmlCodePage.Search, "Result", [element(WbxmlCodePage.Search, "Properties", properties)]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
__decorate([
|
|
124
|
+
Config("mail:eas:search_default_range", 9),
|
|
125
|
+
__metadata("design:type", Number)
|
|
126
|
+
], SearchCommand.prototype, "defaultRangeEnd", void 0);
|
|
127
|
+
__decorate([
|
|
128
|
+
Config("mail:eas:search_max_range", 99),
|
|
129
|
+
__metadata("design:type", Number)
|
|
130
|
+
], SearchCommand.prototype, "maxRangeEnd", void 0);
|
|
131
|
+
__decorate([
|
|
132
|
+
Init,
|
|
133
|
+
__metadata("design:type", Function),
|
|
134
|
+
__metadata("design:paramtypes", []),
|
|
135
|
+
__metadata("design:returntype", Promise)
|
|
136
|
+
], SearchCommand.prototype, "init", null);
|
|
137
|
+
//# sourceMappingURL=SearchCommand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SearchCommand.js","sourceRoot":"","sources":["../../../src/commands/SearchCommand.ts"],"names":[],"mappings":";;;;;;;;;AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAiB,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAChG,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAqB,MAAM,0BAA0B,CAAC;AAGzG,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,gBAAgB,CAAC;AAE1C;;+GAE+G;AAC/G,SAAS,kBAAkB,CAAC,KAAa;IACrC,OAAO,KAAK,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AACxD,CAAC;AAED;gHACgH;AAChH,SAAS,UAAU,CAAC,GAAuB,EAAE,UAAkB,EAAE,MAAc;IAC3E,MAAM,KAAK,GAAG,GAAG,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;IAC1C,IAAI,CAAC,KAAK,EAAE,CAAC;QACT,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,EAAE,CAAC;IAC3D,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC/C,OAAO,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,EAAE,CAAC;AAC3F,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,OAAgB,aAAa;IAAnC;QACoB,YAAO,GAAG,QAAQ,CAAC;QAmB3B,oBAAe,GAAW,CAAC,CAAC;QAG5B,gBAAW,GAAW,EAAE,CAAC;IA+ErC,CAAC;IA5EgB,AAAN,KAAK,CAAC,IAAI;QACb,IAAI,CAAC,WAAW,GAAG,MAAM,IAAI,CAAC,cAAe,CAAC,WAAW,CAAC,SAAS,EAAE;YACjE,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,IAAI;YAC5B,IAAI,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC;SAC5B,CAAC,CAAC;IACP,CAAC;IAEM,KAAK,CAAC,MAAM,CAAC,GAAsB;QACtC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACpB,MAAM,IAAI,QAAQ,CAAC,SAAS,CAAC,cAAc,EAAE,GAAG,EAAE,gBAAgB,CAAC,cAAc,CAAC,CAAC;QACvF,CAAC;QACD,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC1E,MAAM,IAAI,GAAuB,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAClF,MAAM,KAAK,GAAuB,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACpF,IAAI,CAAC,OAAO,IAAI,IAAI,KAAK,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;YACvC,MAAM,IAAI,QAAQ,CAAC,SAAS,CAAC,eAAe,EAAE,GAAG,EAAE,sDAAsD,CAAC,CAAC;QAC/G,CAAC;QAED,MAAM,SAAS,GAAG,SAAS,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;QAChD,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,UAAU,CAC7B,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,EACrD,IAAI,CAAC,eAAe,EACpB,IAAI,CAAC,WAAW,CACnB,CAAC;QAEF,wGAAwG;QACxG,uGAAuG;QACvG,oGAAoG;QACpG,kGAAkG;QAClG,+FAA+F;QAC/F,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5D,MAAM,WAAW,GAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,WAAW,GAAG,CAAC,EAAE,CAAC;QAC1E,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,CAC9B,CAAC,aAAa,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAC7D,IAAI,CAAC,WAAY,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,EAAE,QAAQ,OAAO,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,WAAW,GAAG,CAAC,EAAS,EAAE,WAAW,CAAC,CACvI,CACJ,CAAC;QACF,MAAM,KAAK,GAAG,IAAI,GAAG,EAAqC,CAAC;QAC3D,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;YACpC,KAAK,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACtC,CAAC;QACD,MAAM,OAAO,GAAc,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,aAAa,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;QAEjH,MAAM,IAAI,GAAc,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC;QACtD,kGAAkG;QAClG,uGAAuG;QACvG,4FAA4F;QAC5F,6EAA6E;QAC7E,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QACpD,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAE9E,OAAO,OAAO,CAAC,aAAa,CAAC,MAAM,EAAE,QAAQ,EAAE;YAC3C,WAAW,CAAC,aAAa,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC;YAChD,OAAO,CAAC,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE;gBACtC,OAAO,CAAC,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE;oBACnC,WAAW,CAAC,aAAa,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC;oBAChD,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;oBACvD,WAAW,CAAC,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,UAAU,IAAI,QAAQ,EAAE,CAAC;oBACvE,WAAW,CAAC,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;iBACrE,CAAC;aACL,CAAC;SACL,CAAC,CAAC;IACP,CAAC;IAEO,eAAe,CAAC,OAAgB;QACpC,MAAM,UAAU,GAAmB;YAC/B,WAAW,CAAC,aAAa,CAAC,GAAG,EAAE,aAAa,EAAE,OAAO,CAAC,WAAW,CAAC;YAClE,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,aAAa,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9F,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,aAAa,CAAC,GAAG,EAAE,UAAU,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACzF,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,aAAa,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACxF,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACxF,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACzG,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;SACzG,CAAC;QACF,OAAO,OAAO,CAAC,aAAa,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IAC9G,CAAC;CACJ;AAlFW;IADP,MAAM,CAAC,+BAA+B,EAAE,CAAC,CAAC;;sDACP;AAG5B;IADP,MAAM,CAAC,2BAA2B,EAAE,EAAE,CAAC;;kDACP;AAGpB;IADZ,IAAI;;;;yCAMJ"}
|