@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.
Files changed (195) hide show
  1. package/LICENSE +382 -0
  2. package/README.md +57 -0
  3. package/dist/lib/BaseDeviceSyncStateRoute.js +90 -0
  4. package/dist/lib/BaseDeviceSyncStateRoute.js.map +1 -0
  5. package/dist/lib/BaseEasRoute.js +213 -0
  6. package/dist/lib/BaseEasRoute.js.map +1 -0
  7. package/dist/lib/CompactDateTime.js +43 -0
  8. package/dist/lib/CompactDateTime.js.map +1 -0
  9. package/dist/lib/EasCommandHandler.js +2 -0
  10. package/dist/lib/EasCommandHandler.js.map +1 -0
  11. package/dist/lib/EasSyncKeyUtils.js +119 -0
  12. package/dist/lib/EasSyncKeyUtils.js.map +1 -0
  13. package/dist/lib/adapters/CalendarSyncAdapter.js +255 -0
  14. package/dist/lib/adapters/CalendarSyncAdapter.js.map +1 -0
  15. package/dist/lib/adapters/ContactsSyncAdapter.js +164 -0
  16. package/dist/lib/adapters/ContactsSyncAdapter.js.map +1 -0
  17. package/dist/lib/adapters/EasCollectionSyncAdapter.js +2 -0
  18. package/dist/lib/adapters/EasCollectionSyncAdapter.js.map +1 -0
  19. package/dist/lib/adapters/EmailSyncAdapter.js +230 -0
  20. package/dist/lib/adapters/EmailSyncAdapter.js.map +1 -0
  21. package/dist/lib/adapters/TasksSyncAdapter.js +100 -0
  22. package/dist/lib/adapters/TasksSyncAdapter.js.map +1 -0
  23. package/dist/lib/codec/WbxmlCodePages.js +706 -0
  24. package/dist/lib/codec/WbxmlCodePages.js.map +1 -0
  25. package/dist/lib/codec/WbxmlDecoder.js +154 -0
  26. package/dist/lib/codec/WbxmlDecoder.js.map +1 -0
  27. package/dist/lib/codec/WbxmlElement.js +31 -0
  28. package/dist/lib/codec/WbxmlElement.js.map +1 -0
  29. package/dist/lib/codec/WbxmlEncoder.js +88 -0
  30. package/dist/lib/codec/WbxmlEncoder.js.map +1 -0
  31. package/dist/lib/commands/ComposeMailCommand.js +187 -0
  32. package/dist/lib/commands/ComposeMailCommand.js.map +1 -0
  33. package/dist/lib/commands/FolderSyncCommand.js +150 -0
  34. package/dist/lib/commands/FolderSyncCommand.js.map +1 -0
  35. package/dist/lib/commands/GetItemEstimateCommand.js +149 -0
  36. package/dist/lib/commands/GetItemEstimateCommand.js.map +1 -0
  37. package/dist/lib/commands/ItemOperationsCommand.js +337 -0
  38. package/dist/lib/commands/ItemOperationsCommand.js.map +1 -0
  39. package/dist/lib/commands/MeetingResponseCommand.js +120 -0
  40. package/dist/lib/commands/MeetingResponseCommand.js.map +1 -0
  41. package/dist/lib/commands/MoveItemsCommand.js +103 -0
  42. package/dist/lib/commands/MoveItemsCommand.js.map +1 -0
  43. package/dist/lib/commands/PingCommand.js +145 -0
  44. package/dist/lib/commands/PingCommand.js.map +1 -0
  45. package/dist/lib/commands/ProvisionCommand.js +164 -0
  46. package/dist/lib/commands/ProvisionCommand.js.map +1 -0
  47. package/dist/lib/commands/ResolveRecipientsCommand.js +124 -0
  48. package/dist/lib/commands/ResolveRecipientsCommand.js.map +1 -0
  49. package/dist/lib/commands/SearchCommand.js +137 -0
  50. package/dist/lib/commands/SearchCommand.js.map +1 -0
  51. package/dist/lib/commands/SendMailCommand.js +18 -0
  52. package/dist/lib/commands/SendMailCommand.js.map +1 -0
  53. package/dist/lib/commands/SettingsCommand.js +138 -0
  54. package/dist/lib/commands/SettingsCommand.js.map +1 -0
  55. package/dist/lib/commands/SmartForwardCommand.js +22 -0
  56. package/dist/lib/commands/SmartForwardCommand.js.map +1 -0
  57. package/dist/lib/commands/SmartReplyCommand.js +22 -0
  58. package/dist/lib/commands/SmartReplyCommand.js.map +1 -0
  59. package/dist/lib/commands/SyncCommand.js +406 -0
  60. package/dist/lib/commands/SyncCommand.js.map +1 -0
  61. package/dist/lib/commands/mongo/FolderSyncCommandMongo.js +16 -0
  62. package/dist/lib/commands/mongo/FolderSyncCommandMongo.js.map +1 -0
  63. package/dist/lib/commands/mongo/GetItemEstimateCommandMongo.js +21 -0
  64. package/dist/lib/commands/mongo/GetItemEstimateCommandMongo.js.map +1 -0
  65. package/dist/lib/commands/mongo/ItemOperationsCommandMongo.js +18 -0
  66. package/dist/lib/commands/mongo/ItemOperationsCommandMongo.js.map +1 -0
  67. package/dist/lib/commands/mongo/MeetingResponseCommandMongo.js +17 -0
  68. package/dist/lib/commands/mongo/MeetingResponseCommandMongo.js.map +1 -0
  69. package/dist/lib/commands/mongo/MoveItemsCommandMongo.js +17 -0
  70. package/dist/lib/commands/mongo/MoveItemsCommandMongo.js.map +1 -0
  71. package/dist/lib/commands/mongo/ResolveRecipientsCommandMongo.js +20 -0
  72. package/dist/lib/commands/mongo/ResolveRecipientsCommandMongo.js.map +1 -0
  73. package/dist/lib/commands/mongo/SearchCommandMongo.js +20 -0
  74. package/dist/lib/commands/mongo/SearchCommandMongo.js.map +1 -0
  75. package/dist/lib/commands/mongo/SendMailCommandMongo.js +17 -0
  76. package/dist/lib/commands/mongo/SendMailCommandMongo.js.map +1 -0
  77. package/dist/lib/commands/mongo/SettingsCommandMongo.js +16 -0
  78. package/dist/lib/commands/mongo/SettingsCommandMongo.js.map +1 -0
  79. package/dist/lib/commands/mongo/SmartForwardCommandMongo.js +17 -0
  80. package/dist/lib/commands/mongo/SmartForwardCommandMongo.js.map +1 -0
  81. package/dist/lib/commands/mongo/SmartReplyCommandMongo.js +17 -0
  82. package/dist/lib/commands/mongo/SmartReplyCommandMongo.js.map +1 -0
  83. package/dist/lib/commands/mongo/SyncCommandMongo.js +26 -0
  84. package/dist/lib/commands/mongo/SyncCommandMongo.js.map +1 -0
  85. package/dist/lib/commands/mongo/index.js +14 -0
  86. package/dist/lib/commands/mongo/index.js.map +1 -0
  87. package/dist/lib/commands/sql/FolderSyncCommandSQL.js +16 -0
  88. package/dist/lib/commands/sql/FolderSyncCommandSQL.js.map +1 -0
  89. package/dist/lib/commands/sql/GetItemEstimateCommandSQL.js +21 -0
  90. package/dist/lib/commands/sql/GetItemEstimateCommandSQL.js.map +1 -0
  91. package/dist/lib/commands/sql/ItemOperationsCommandSQL.js +18 -0
  92. package/dist/lib/commands/sql/ItemOperationsCommandSQL.js.map +1 -0
  93. package/dist/lib/commands/sql/MeetingResponseCommandSQL.js +17 -0
  94. package/dist/lib/commands/sql/MeetingResponseCommandSQL.js.map +1 -0
  95. package/dist/lib/commands/sql/MoveItemsCommandSQL.js +17 -0
  96. package/dist/lib/commands/sql/MoveItemsCommandSQL.js.map +1 -0
  97. package/dist/lib/commands/sql/ResolveRecipientsCommandSQL.js +20 -0
  98. package/dist/lib/commands/sql/ResolveRecipientsCommandSQL.js.map +1 -0
  99. package/dist/lib/commands/sql/SearchCommandSQL.js +20 -0
  100. package/dist/lib/commands/sql/SearchCommandSQL.js.map +1 -0
  101. package/dist/lib/commands/sql/SendMailCommandSQL.js +17 -0
  102. package/dist/lib/commands/sql/SendMailCommandSQL.js.map +1 -0
  103. package/dist/lib/commands/sql/SettingsCommandSQL.js +16 -0
  104. package/dist/lib/commands/sql/SettingsCommandSQL.js.map +1 -0
  105. package/dist/lib/commands/sql/SmartForwardCommandSQL.js +17 -0
  106. package/dist/lib/commands/sql/SmartForwardCommandSQL.js.map +1 -0
  107. package/dist/lib/commands/sql/SmartReplyCommandSQL.js +17 -0
  108. package/dist/lib/commands/sql/SmartReplyCommandSQL.js.map +1 -0
  109. package/dist/lib/commands/sql/SyncCommandSQL.js +26 -0
  110. package/dist/lib/commands/sql/SyncCommandSQL.js.map +1 -0
  111. package/dist/lib/commands/sql/index.js +14 -0
  112. package/dist/lib/commands/sql/index.js.map +1 -0
  113. package/dist/lib/index.js +56 -0
  114. package/dist/lib/index.js.map +1 -0
  115. package/dist/lib/mongo/DeviceSyncStateRouteMongo.js +16 -0
  116. package/dist/lib/mongo/DeviceSyncStateRouteMongo.js.map +1 -0
  117. package/dist/lib/mongo/EasRouteMongo.js +51 -0
  118. package/dist/lib/mongo/EasRouteMongo.js.map +1 -0
  119. package/dist/lib/mongo/index.js +8 -0
  120. package/dist/lib/mongo/index.js.map +1 -0
  121. package/dist/lib/mongo.js +6 -0
  122. package/dist/lib/mongo.js.map +1 -0
  123. package/dist/lib/sql/DeviceSyncStateRouteSQL.js +16 -0
  124. package/dist/lib/sql/DeviceSyncStateRouteSQL.js.map +1 -0
  125. package/dist/lib/sql/EasRouteSQL.js +50 -0
  126. package/dist/lib/sql/EasRouteSQL.js.map +1 -0
  127. package/dist/lib/sql/index.js +8 -0
  128. package/dist/lib/sql/index.js.map +1 -0
  129. package/dist/lib/sql.js +6 -0
  130. package/dist/lib/sql.js.map +1 -0
  131. package/dist/types/BaseDeviceSyncStateRoute.d.ts +34 -0
  132. package/dist/types/BaseEasRoute.d.ts +64 -0
  133. package/dist/types/CompactDateTime.d.ts +22 -0
  134. package/dist/types/EasCommandHandler.d.ts +59 -0
  135. package/dist/types/EasSyncKeyUtils.d.ts +89 -0
  136. package/dist/types/adapters/CalendarSyncAdapter.d.ts +52 -0
  137. package/dist/types/adapters/ContactsSyncAdapter.d.ts +33 -0
  138. package/dist/types/adapters/EasCollectionSyncAdapter.d.ts +52 -0
  139. package/dist/types/adapters/EmailSyncAdapter.d.ts +53 -0
  140. package/dist/types/adapters/TasksSyncAdapter.d.ts +32 -0
  141. package/dist/types/codec/WbxmlCodePages.d.ts +45 -0
  142. package/dist/types/codec/WbxmlDecoder.d.ts +33 -0
  143. package/dist/types/codec/WbxmlElement.d.ts +43 -0
  144. package/dist/types/codec/WbxmlEncoder.d.ts +20 -0
  145. package/dist/types/commands/ComposeMailCommand.d.ts +50 -0
  146. package/dist/types/commands/FolderSyncCommand.d.ts +25 -0
  147. package/dist/types/commands/GetItemEstimateCommand.d.ts +43 -0
  148. package/dist/types/commands/ItemOperationsCommand.d.ts +72 -0
  149. package/dist/types/commands/MeetingResponseCommand.d.ts +32 -0
  150. package/dist/types/commands/MoveItemsCommand.d.ts +29 -0
  151. package/dist/types/commands/PingCommand.d.ts +33 -0
  152. package/dist/types/commands/ProvisionCommand.d.ts +54 -0
  153. package/dist/types/commands/ResolveRecipientsCommand.d.ts +34 -0
  154. package/dist/types/commands/SearchCommand.d.ts +39 -0
  155. package/dist/types/commands/SendMailCommand.d.ts +10 -0
  156. package/dist/types/commands/SettingsCommand.d.ts +37 -0
  157. package/dist/types/commands/SmartForwardCommand.d.ts +17 -0
  158. package/dist/types/commands/SmartReplyCommand.d.ts +17 -0
  159. package/dist/types/commands/SyncCommand.d.ts +100 -0
  160. package/dist/types/commands/mongo/FolderSyncCommandMongo.d.ts +8 -0
  161. package/dist/types/commands/mongo/GetItemEstimateCommandMongo.d.ts +7 -0
  162. package/dist/types/commands/mongo/ItemOperationsCommandMongo.d.ts +9 -0
  163. package/dist/types/commands/mongo/MeetingResponseCommandMongo.d.ts +8 -0
  164. package/dist/types/commands/mongo/MoveItemsCommandMongo.d.ts +8 -0
  165. package/dist/types/commands/mongo/ResolveRecipientsCommandMongo.d.ts +8 -0
  166. package/dist/types/commands/mongo/SearchCommandMongo.d.ts +8 -0
  167. package/dist/types/commands/mongo/SendMailCommandMongo.d.ts +8 -0
  168. package/dist/types/commands/mongo/SettingsCommandMongo.d.ts +7 -0
  169. package/dist/types/commands/mongo/SmartForwardCommandMongo.d.ts +8 -0
  170. package/dist/types/commands/mongo/SmartReplyCommandMongo.d.ts +8 -0
  171. package/dist/types/commands/mongo/SyncCommandMongo.d.ts +8 -0
  172. package/dist/types/commands/mongo/index.d.ts +9 -0
  173. package/dist/types/commands/sql/FolderSyncCommandSQL.d.ts +8 -0
  174. package/dist/types/commands/sql/GetItemEstimateCommandSQL.d.ts +7 -0
  175. package/dist/types/commands/sql/ItemOperationsCommandSQL.d.ts +9 -0
  176. package/dist/types/commands/sql/MeetingResponseCommandSQL.d.ts +8 -0
  177. package/dist/types/commands/sql/MoveItemsCommandSQL.d.ts +8 -0
  178. package/dist/types/commands/sql/ResolveRecipientsCommandSQL.d.ts +8 -0
  179. package/dist/types/commands/sql/SearchCommandSQL.d.ts +8 -0
  180. package/dist/types/commands/sql/SendMailCommandSQL.d.ts +8 -0
  181. package/dist/types/commands/sql/SettingsCommandSQL.d.ts +7 -0
  182. package/dist/types/commands/sql/SmartForwardCommandSQL.d.ts +8 -0
  183. package/dist/types/commands/sql/SmartReplyCommandSQL.d.ts +8 -0
  184. package/dist/types/commands/sql/SyncCommandSQL.d.ts +8 -0
  185. package/dist/types/commands/sql/index.d.ts +9 -0
  186. package/dist/types/index.d.ts +51 -0
  187. package/dist/types/mongo/DeviceSyncStateRouteMongo.d.ts +8 -0
  188. package/dist/types/mongo/EasRouteMongo.d.ts +14 -0
  189. package/dist/types/mongo/index.d.ts +3 -0
  190. package/dist/types/mongo.d.ts +1 -0
  191. package/dist/types/sql/DeviceSyncStateRouteSQL.d.ts +8 -0
  192. package/dist/types/sql/EasRouteSQL.d.ts +13 -0
  193. package/dist/types/sql/index.d.ts +3 -0
  194. package/dist/types/sql.d.ts +1 -0
  195. 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":"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"}
@@ -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":"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"}