@rapidmx/activesync-plugin 1.0.0-beta.2
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 +269 -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/adapters/mongo/EmailSyncAdapterMongo.js +16 -0
- package/dist/lib/adapters/mongo/EmailSyncAdapterMongo.js.map +1 -0
- package/dist/lib/adapters/mongo/index.js +6 -0
- package/dist/lib/adapters/mongo/index.js.map +1 -0
- package/dist/lib/adapters/sql/EmailSyncAdapterSQL.js +16 -0
- package/dist/lib/adapters/sql/EmailSyncAdapterSQL.js.map +1 -0
- package/dist/lib/adapters/sql/index.js +6 -0
- package/dist/lib/adapters/sql/index.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 +155 -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 +123 -0
- package/dist/lib/commands/ResolveRecipientsCommand.js.map +1 -0
- package/dist/lib/commands/SearchCommand.js +234 -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 +16 -0
- package/dist/lib/commands/mongo/ResolveRecipientsCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/SearchCommandMongo.js +19 -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 +16 -0
- package/dist/lib/commands/sql/ResolveRecipientsCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/SearchCommandSQL.js +19 -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 +58 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/jobs/EasDeviceStateCleanupJob.js +101 -0
- package/dist/lib/jobs/EasDeviceStateCleanupJob.js.map +1 -0
- package/dist/lib/jobs/mongo/EasDeviceStateCleanupJobMongo.js +13 -0
- package/dist/lib/jobs/mongo/EasDeviceStateCleanupJobMongo.js.map +1 -0
- package/dist/lib/jobs/sql/EasDeviceStateCleanupJobSQL.js +13 -0
- package/dist/lib/jobs/sql/EasDeviceStateCleanupJobSQL.js.map +1 -0
- package/dist/lib/models/DeviceSyncState.js +2 -0
- package/dist/lib/models/DeviceSyncState.js.map +1 -0
- package/dist/lib/models/mongo/DeviceSyncStateMongo.js +130 -0
- package/dist/lib/models/mongo/DeviceSyncStateMongo.js.map +1 -0
- package/dist/lib/models/sql/DeviceSyncStateSQL.js +130 -0
- package/dist/lib/models/sql/DeviceSyncStateSQL.js.map +1 -0
- package/dist/lib/mongo/DeviceSyncStateRouteMongo.js +28 -0
- package/dist/lib/mongo/DeviceSyncStateRouteMongo.js.map +1 -0
- package/dist/lib/mongo/EasRouteMongo.js +63 -0
- package/dist/lib/mongo/EasRouteMongo.js.map +1 -0
- package/dist/lib/mongo/index.js +9 -0
- package/dist/lib/mongo/index.js.map +1 -0
- package/dist/lib/mongo.js +15 -0
- package/dist/lib/mongo.js.map +1 -0
- package/dist/lib/sql/DeviceSyncStateRouteSQL.js +28 -0
- package/dist/lib/sql/DeviceSyncStateRouteSQL.js.map +1 -0
- package/dist/lib/sql/EasRouteSQL.js +63 -0
- package/dist/lib/sql/EasRouteSQL.js.map +1 -0
- package/dist/lib/sql/index.js +9 -0
- package/dist/lib/sql/index.js.map +1 -0
- package/dist/lib/sql.js +15 -0
- package/dist/lib/sql.js.map +1 -0
- package/dist/types/BaseDeviceSyncStateRoute.d.ts +34 -0
- package/dist/types/BaseEasRoute.d.ts +65 -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 +55 -0
- package/dist/types/adapters/EmailSyncAdapter.d.ts +71 -0
- package/dist/types/adapters/TasksSyncAdapter.d.ts +32 -0
- package/dist/types/adapters/mongo/EmailSyncAdapterMongo.d.ts +7 -0
- package/dist/types/adapters/mongo/index.d.ts +1 -0
- package/dist/types/adapters/sql/EmailSyncAdapterSQL.d.ts +7 -0
- package/dist/types/adapters/sql/index.d.ts +1 -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 +36 -0
- package/dist/types/commands/SearchCommand.d.ts +60 -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 +7 -0
- package/dist/types/commands/mongo/SearchCommandMongo.d.ts +9 -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 +7 -0
- package/dist/types/commands/sql/SearchCommandSQL.d.ts +9 -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 +53 -0
- package/dist/types/jobs/EasDeviceStateCleanupJob.d.ts +26 -0
- package/dist/types/jobs/mongo/EasDeviceStateCleanupJobMongo.d.ts +5 -0
- package/dist/types/jobs/sql/EasDeviceStateCleanupJobSQL.d.ts +5 -0
- package/dist/types/models/DeviceSyncState.d.ts +30 -0
- package/dist/types/models/mongo/DeviceSyncStateMongo.d.ts +22 -0
- package/dist/types/models/sql/DeviceSyncStateSQL.d.ts +22 -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 +4 -0
- package/dist/types/mongo.d.ts +10 -0
- package/dist/types/sql/DeviceSyncStateRouteSQL.d.ts +8 -0
- package/dist/types/sql/EasRouteSQL.d.ts +14 -0
- package/dist/types/sql/index.d.ts +4 -0
- package/dist/types/sql.d.ts +10 -0
- package/package.json +238 -0
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
8
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
9
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
10
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
11
|
+
import { MailboxSQL } from "@rapidmx/restapi/sql";
|
|
12
|
+
import { RouteDecorators } from "@rapidrest/service-core";
|
|
13
|
+
import { DeviceSyncStateSQL } from "../models/sql/DeviceSyncStateSQL.js";
|
|
14
|
+
import { BaseEasRoute } from "../BaseEasRoute.js";
|
|
15
|
+
import { ProvisionCommand } from "../commands/ProvisionCommand.js";
|
|
16
|
+
import { PingCommand } from "../commands/PingCommand.js";
|
|
17
|
+
import { FolderSyncCommandSQL } from "../commands/sql/FolderSyncCommandSQL.js";
|
|
18
|
+
import { SyncCommandSQL } from "../commands/sql/SyncCommandSQL.js";
|
|
19
|
+
import { SendMailCommandSQL } from "../commands/sql/SendMailCommandSQL.js";
|
|
20
|
+
import { SmartForwardCommandSQL } from "../commands/sql/SmartForwardCommandSQL.js";
|
|
21
|
+
import { SmartReplyCommandSQL } from "../commands/sql/SmartReplyCommandSQL.js";
|
|
22
|
+
import { ItemOperationsCommandSQL } from "../commands/sql/ItemOperationsCommandSQL.js";
|
|
23
|
+
import { SearchCommandSQL } from "../commands/sql/SearchCommandSQL.js";
|
|
24
|
+
import { MeetingResponseCommandSQL } from "../commands/sql/MeetingResponseCommandSQL.js";
|
|
25
|
+
import { SettingsCommandSQL } from "../commands/sql/SettingsCommandSQL.js";
|
|
26
|
+
import { GetItemEstimateCommandSQL } from "../commands/sql/GetItemEstimateCommandSQL.js";
|
|
27
|
+
import { MoveItemsCommandSQL } from "../commands/sql/MoveItemsCommandSQL.js";
|
|
28
|
+
import { ResolveRecipientsCommandSQL } from "../commands/sql/ResolveRecipientsCommandSQL.js";
|
|
29
|
+
const { Route } = RouteDecorators;
|
|
30
|
+
/**
|
|
31
|
+
* SQL-backed concrete `BaseEasRoute`, mounted at the protocol's well-known path. Exported from this plugin's
|
|
32
|
+
* `./sql` entry point.
|
|
33
|
+
*
|
|
34
|
+
* @author Jean-Philippe Steinmetz
|
|
35
|
+
*/
|
|
36
|
+
let EasRouteSQL = class EasRouteSQL extends BaseEasRoute {
|
|
37
|
+
constructor() {
|
|
38
|
+
super(...arguments);
|
|
39
|
+
this.deviceSyncStateClass = DeviceSyncStateSQL;
|
|
40
|
+
this.mailboxClass = MailboxSQL;
|
|
41
|
+
this.commandHandlerClasses = [
|
|
42
|
+
ProvisionCommand,
|
|
43
|
+
FolderSyncCommandSQL,
|
|
44
|
+
SyncCommandSQL,
|
|
45
|
+
SendMailCommandSQL,
|
|
46
|
+
SmartForwardCommandSQL,
|
|
47
|
+
SmartReplyCommandSQL,
|
|
48
|
+
ItemOperationsCommandSQL,
|
|
49
|
+
SearchCommandSQL,
|
|
50
|
+
MeetingResponseCommandSQL,
|
|
51
|
+
SettingsCommandSQL,
|
|
52
|
+
PingCommand,
|
|
53
|
+
GetItemEstimateCommandSQL,
|
|
54
|
+
MoveItemsCommandSQL,
|
|
55
|
+
ResolveRecipientsCommandSQL,
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
EasRouteSQL = __decorate([
|
|
60
|
+
Route("/Microsoft-Server-ActiveSync")
|
|
61
|
+
], EasRouteSQL);
|
|
62
|
+
export { EasRouteSQL };
|
|
63
|
+
//# sourceMappingURL=EasRouteSQL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EasRouteSQL.js","sourceRoot":"","sources":["../../../src/sql/EasRouteSQL.ts"],"names":[],"mappings":";;;;;;AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,yCAAyC,CAAC;AAC/E,OAAO,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAC3E,OAAO,EAAE,sBAAsB,EAAE,MAAM,2CAA2C,CAAC;AACnF,OAAO,EAAE,oBAAoB,EAAE,MAAM,yCAAyC,CAAC;AAC/E,OAAO,EAAE,wBAAwB,EAAE,MAAM,6CAA6C,CAAC;AACvF,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AACvE,OAAO,EAAE,yBAAyB,EAAE,MAAM,8CAA8C,CAAC;AACzF,OAAO,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAC3E,OAAO,EAAE,yBAAyB,EAAE,MAAM,8CAA8C,CAAC;AACzF,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAC7E,OAAO,EAAE,2BAA2B,EAAE,MAAM,gDAAgD,CAAC;AAC7F,MAAM,EAAE,KAAK,EAAE,GAAG,eAAe,CAAC;AAElC;;;;;GAKG;AAEI,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,YAA4C;IAAtE;;QACO,yBAAoB,GAAQ,kBAAkB,CAAC;QAC/C,iBAAY,GAAQ,UAAU,CAAC;QAC/B,0BAAqB,GAAU;YACrC,gBAAgB;YAChB,oBAAoB;YACpB,cAAc;YACd,kBAAkB;YAClB,sBAAsB;YACtB,oBAAoB;YACpB,wBAAwB;YACxB,gBAAgB;YAChB,yBAAyB;YACzB,kBAAkB;YAClB,WAAW;YACX,yBAAyB;YACzB,mBAAmB;YACnB,2BAA2B;SAC9B,CAAC;IACN,CAAC;CAAA,CAAA;AAnBY,WAAW;IADvB,KAAK,CAAC,8BAA8B,CAAC;GACzB,WAAW,CAmBvB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
export * from "./EasRouteSQL.js";
|
|
6
|
+
export * from "./DeviceSyncStateRouteSQL.js";
|
|
7
|
+
export * from "../commands/sql/index.js";
|
|
8
|
+
export * from "../adapters/sql/index.js";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/sql/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,cAAc,kBAAkB,CAAC;AACjC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC"}
|
package/dist/lib/sql.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/**
|
|
6
|
+
* This plugin's `./sql` entry point: exactly the classes a server host loads for a SQL deployment - the
|
|
7
|
+
* mounted routes, the device-state model and its cleanup job. Anything else exported here would be registered
|
|
8
|
+
* by the host too (and an abstract `BackgroundService` would even be started), so the command and adapter
|
|
9
|
+
* classes these routes use stay internal.
|
|
10
|
+
*/
|
|
11
|
+
export { EasRouteSQL } from "./sql/EasRouteSQL.js";
|
|
12
|
+
export { DeviceSyncStateRouteSQL } from "./sql/DeviceSyncStateRouteSQL.js";
|
|
13
|
+
export { DeviceSyncStateSQL } from "./models/sql/DeviceSyncStateSQL.js";
|
|
14
|
+
export { EasDeviceStateCleanupJobSQL } from "./jobs/sql/EasDeviceStateCleanupJobSQL.js";
|
|
15
|
+
//# sourceMappingURL=sql.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql.js","sourceRoot":"","sources":["../../src/sql.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E;;;;;GAKG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,EAAE,2BAA2B,EAAE,MAAM,2CAA2C,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type JWTUser } from "@rapidrest/core";
|
|
2
|
+
import { HttpRequest } from "@rapidrest/service-core";
|
|
3
|
+
import type { DeviceSyncState } from "./models/DeviceSyncState.js";
|
|
4
|
+
/**
|
|
5
|
+
* Admin-only trigger for the `RemoteWipe` sub-flow `ProvisionCommand` implements. `restapi` has no route of its
|
|
6
|
+
* own for `DeviceSyncState` (it's a protocol-internal entity, not a domain object a normal API consumer ever
|
|
7
|
+
* lists or edits directly), so this lives here instead, following the exact `trustedRoles`/`UserUtils.hasRoles`
|
|
8
|
+
* gating pattern `BaseMailboxRoute` already uses for its own admin-vs-owner scoping.
|
|
9
|
+
*
|
|
10
|
+
* Setting `remoteWipeRequested: true` (and `provisioned: false` alongside it) is the entire trigger: the next
|
|
11
|
+
* request that device makes of any kind is already forced back through `Provision` by `BaseEasRoute.dispatch()`'s
|
|
12
|
+
* existing 449 gate, at which point `ProvisionCommand.issuePolicy` sees the flag and sends the device a
|
|
13
|
+
* `RemoteWipe` directive instead of a normal policy document - no separate push/notification channel is needed.
|
|
14
|
+
*
|
|
15
|
+
* `deviceSyncStateClass` is supplied by the Mongo/SQL concrete subclasses, the same one-line-per-backend
|
|
16
|
+
* pattern used throughout this library.
|
|
17
|
+
*
|
|
18
|
+
* @author Jean-Philippe Steinmetz
|
|
19
|
+
*/
|
|
20
|
+
export declare abstract class BaseDeviceSyncStateRoute<D extends DeviceSyncState> {
|
|
21
|
+
protected abstract deviceSyncStateClass: any;
|
|
22
|
+
protected trustedRoles: string[];
|
|
23
|
+
private _objectFactory?;
|
|
24
|
+
private deviceSyncStateRepo?;
|
|
25
|
+
private getDeviceSyncStateRepo;
|
|
26
|
+
/**
|
|
27
|
+
* Marks `uid` (a `DeviceSyncState`'s own id, not a device id string - the admin looks this up via whatever
|
|
28
|
+
* device-listing view a deployment builds on top of `DeviceSyncState`'s ordinary CRUD, out of scope here)
|
|
29
|
+
* for remote wipe. Optional JSON body `{ accountOnly?: boolean }` — recorded for admin audit only; see
|
|
30
|
+
* `ProvisionCommand`'s own doc comment for why the wire directive sent to the device doesn't distinguish
|
|
31
|
+
* the two.
|
|
32
|
+
*/
|
|
33
|
+
remoteWipe(uid: string, req: HttpRequest, user?: JWTUser): Promise<D>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type JWTUser } from "@rapidrest/core";
|
|
2
|
+
import { HttpRequest, HttpResponse } from "@rapidrest/service-core";
|
|
3
|
+
import { Mailbox } from "@rapidmx/restapi";
|
|
4
|
+
import { DeviceSyncState } from "./models/DeviceSyncState.js";
|
|
5
|
+
/**
|
|
6
|
+
* Abstract base for the single fixed EAS endpoint (`POST /Microsoft-Server-ActiveSync` by MS-ASHTTP
|
|
7
|
+
* convention, though the concrete path is left to the consuming application to mount via `@Route(...)` — see
|
|
8
|
+
* `BaseMailIngestRoute.ts` for the identical undecorated-base-class pattern this follows). Unlike every other
|
|
9
|
+
* route in this library, EAS dispatches on a `Cmd` query parameter against one URL rather than path-based REST
|
|
10
|
+
* routing, so there is exactly one `@Post()` method here, not one per operation.
|
|
11
|
+
*
|
|
12
|
+
* **Auth**: `@Auth(["jwt"])` — the framework's own default strategy, unchanged from every other route in this
|
|
13
|
+
* app. A real native EAS client obtaining that JWT in the first place (rather than the app's own web/API
|
|
14
|
+
* clients, which already have one) requires an OAuth 2.0 Authorization Server capability this library
|
|
15
|
+
* deliberately does not implement itself — see the architecture plan's "Auth" section for the full reasoning
|
|
16
|
+
* behind this choice over a per-request Basic Auth strategy.
|
|
17
|
+
*
|
|
18
|
+
* **Dispatch flow**: resolve the caller's own `Mailbox` (never a client-supplied one — `resolveCallerMailboxUid`,
|
|
19
|
+
* the same helper `BaseSearchRoute` already uses), find-or-create that (mailbox, device) pair's
|
|
20
|
+
* `DeviceSyncState`, enforce the provisioning gate, decode the WBXML request body (if any), dispatch to the
|
|
21
|
+
* matching registered `EasCommandHandler`, bump `lastSyncAt`, and encode the handler's response back to WBXML.
|
|
22
|
+
*
|
|
23
|
+
* **Command handlers** are supplied via `commandHandlerClasses` (empty by default — this class alone is just
|
|
24
|
+
* the transport skeleton; concrete command support, e.g. `ProvisionCommand`/`FolderSyncCommand`, is added
|
|
25
|
+
* incrementally in later work by having a concrete subclass populate this array) and instantiated once each in
|
|
26
|
+
* `@Init` via `ObjectFactory`, so a handler can `@Inject` its own dependencies like any other DI-managed class
|
|
27
|
+
* in this library.
|
|
28
|
+
*
|
|
29
|
+
* **`OPTIONS` protocol discovery**: real EAS clients conventionally probe `OPTIONS` before their first `POST`
|
|
30
|
+
* to read `MS-ASProtocolVersions`/`MS-ASProtocolCommands` and learn what the server supports - see `options()`
|
|
31
|
+
* below. This requires `@rapidrest/service-core` >=1.5.0, whose global CORS middleware consults
|
|
32
|
+
* `IHttpRouter.hasExplicitOptionsRoute()` before its blanket preflight `204` - confirmed live end to end
|
|
33
|
+
* against a real `service-core` 1.5.0 install (`test/routes/{mongo,sql}/EasRoute.test.ts`'s `OPTIONS`
|
|
34
|
+
* describe block), not assumed.
|
|
35
|
+
*
|
|
36
|
+
* `deviceSyncStateClass`/`mailboxClass` are supplied by the Mongo/SQL concrete subclasses, following the exact
|
|
37
|
+
* one-line-per-backend pattern used throughout this library's other routes/jobs.
|
|
38
|
+
*
|
|
39
|
+
* @author Jean-Philippe Steinmetz
|
|
40
|
+
*/
|
|
41
|
+
export declare abstract class BaseEasRoute<D extends DeviceSyncState, M extends Mailbox = Mailbox> {
|
|
42
|
+
protected abstract deviceSyncStateClass: any;
|
|
43
|
+
protected abstract mailboxClass: any;
|
|
44
|
+
/** Command handler classes to instantiate (one each) in `@Init`. Empty until a concrete command lands -
|
|
45
|
+
* every request is then answered with HTTP 501 (see `dispatch()`), which is the correct, honest behavior
|
|
46
|
+
* for a transport skeleton with no commands implemented yet, not a bug to work around. */
|
|
47
|
+
protected commandHandlerClasses: any[];
|
|
48
|
+
private _objectFactory?;
|
|
49
|
+
private deviceSyncStateRepo?;
|
|
50
|
+
private mailboxRepo?;
|
|
51
|
+
private readonly handlers;
|
|
52
|
+
private logger;
|
|
53
|
+
init(): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Answers a real client's pre-flight `MS-ASProtocolVersions`/`MS-ASProtocolCommands` capability probe -
|
|
56
|
+
* see this class's own doc comment for the `service-core` version dependency this needs to actually run.
|
|
57
|
+
* Deliberately unauthenticated (no `@Auth`): this is capability discovery, not mailbox access, and a real
|
|
58
|
+
* Exchange server answers it the same way regardless of credentials. `MS-ASProtocolCommands` is built from
|
|
59
|
+
* `this.handlers`, not a separately-maintained list, so it can never drift out of sync with the commands a
|
|
60
|
+
* concrete subclass actually registered via `commandHandlerClasses`.
|
|
61
|
+
*/
|
|
62
|
+
options(res: HttpResponse): Promise<void>;
|
|
63
|
+
dispatch(req: HttpRequest, res: HttpResponse, user?: JWTUser): Promise<void>;
|
|
64
|
+
private findOrCreateDeviceSyncState;
|
|
65
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats a date as MS-ASDTYPE's "Compact DateTime" (`YYYYMMDDTHHMMSSZ`, always UTC) - the format
|
|
3
|
+
* `Calendar`/`Tasks` timestamp fields (`StartTime`/`EndTime`/`UtcDueDate`/...) use, distinct from the plain
|
|
4
|
+
* `dateTime` type (`YYYY-MM-DDTHH:MM:SS.MSSZ`, i.e. `Date.prototype.toISOString()`) `Email`'s `DateReceived`
|
|
5
|
+
* uses - confirmed against the published MS-ASDTYPE spec (`2.7.2 Compact DateTime` vs `2.7 dateTime Data
|
|
6
|
+
* Type`), not assumed, since sending the wrong one is exactly the kind of silent-until-a-real-device-connects
|
|
7
|
+
* bug this library's WBXML codec work already ran into once with tag casing (`"Mime"` vs `"MIME"`).
|
|
8
|
+
*
|
|
9
|
+
* Accepts `Date | string` and normalizes via `new Date(...)` - a real, discovered-by-testing gap: fields
|
|
10
|
+
* embedded inside a `simple-json` column (e.g. `RecurrenceRule.until`) round-trip through `JSON.stringify`/
|
|
11
|
+
* `JSON.parse` on the SQL backend, which does not preserve `Date` instances, so `until` comes back as a plain
|
|
12
|
+
* ISO string there even though the Mongo backend (native BSON dates) hands back a real `Date` for the exact
|
|
13
|
+
* same field - a caller passing either must work on both backends without knowing which one it's talking to.
|
|
14
|
+
*/
|
|
15
|
+
export declare function toCompactDateTime(date: Date | string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Parses MS-ASDTYPE's "Compact DateTime" (`YYYYMMDDTHHMMSSZ`) back into a `Date` - the reverse of
|
|
18
|
+
* `toCompactDateTime`, needed for `Sync`'s client-originated `Add`/`Change` commands on `Calendar`/`Tasks`.
|
|
19
|
+
* Throws (rather than returning an unvalidated `Date` that would silently carry `NaN`s) for anything not
|
|
20
|
+
* matching the exact expected shape.
|
|
21
|
+
*/
|
|
22
|
+
export declare function fromCompactDateTime(value: string): Date;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { JWTUser } from "@rapidrest/core";
|
|
2
|
+
import type { HttpRequest, RepoUtils } from "@rapidrest/service-core";
|
|
3
|
+
import type { WbxmlElement } from "./codec/WbxmlElement.js";
|
|
4
|
+
import type { DeviceSyncState } from "./models/DeviceSyncState.js";
|
|
5
|
+
/**
|
|
6
|
+
* Everything an `EasCommandHandler` needs to process one dispatched EAS command — assembled once by
|
|
7
|
+
* `BaseEasRoute.dispatch()` per request and handed to whichever handler matches `?Cmd=`.
|
|
8
|
+
*
|
|
9
|
+
* @author Jean-Philippe Steinmetz
|
|
10
|
+
*/
|
|
11
|
+
export interface EasCommandContext {
|
|
12
|
+
/** The authenticated caller, per the same `@AuthUser` JWT payload every other route in this library reads. */
|
|
13
|
+
readonly user: JWTUser;
|
|
14
|
+
/** The `Mailbox` this request operates against — resolved server-side from `user` (`ownerUserUid`), never
|
|
15
|
+
* taken from client input. */
|
|
16
|
+
readonly mailboxUid: string;
|
|
17
|
+
/** The client-supplied `?DeviceId=` query value, identifying this device within the mailbox. */
|
|
18
|
+
readonly deviceId: string;
|
|
19
|
+
/** The client-supplied `?DeviceType=` query value (e.g. `iPhone`, `Android`). */
|
|
20
|
+
readonly deviceType: string;
|
|
21
|
+
/** The client-supplied `?PolicyKey=` query value, if present - the provisioning policy key the device is
|
|
22
|
+
* currently operating under. Not yet validated against `deviceSyncState.policyKey` here (deferred to
|
|
23
|
+
* `ProvisionCommand`'s own implementation); handlers that care should compare it themselves for now. */
|
|
24
|
+
readonly policyKey?: string;
|
|
25
|
+
/** This device's persisted sync/provisioning state, looked up (or newly created) by `BaseEasRoute` before
|
|
26
|
+
* dispatch. Handlers read/write cursor and provisioning fields on this directly. */
|
|
27
|
+
readonly deviceSyncState: DeviceSyncState;
|
|
28
|
+
/** The live repo backing `deviceSyncState`'s concrete entity class, for handlers that need to persist a
|
|
29
|
+
* change to it (e.g. a new `SyncKey`, an updated `policyKey`, flipping `provisioned`). Typed `RepoUtils<any>`
|
|
30
|
+
* rather than `RepoUtils<DeviceSyncState>` deliberately - `RepoUtils<D>`'s underlying TypeORM `Repository<D>`
|
|
31
|
+
* is not covariant in `D` (a handful of its methods, e.g. `sum()`, use `D`-dependent conditional types), so
|
|
32
|
+
* `BaseEasRoute`'s own `RepoUtils<D>` (for its concrete `D extends DeviceSyncState`) cannot be narrowed to
|
|
33
|
+
* this field's type without `any` somewhere in between. */
|
|
34
|
+
readonly deviceSyncStateRepo: RepoUtils<any>;
|
|
35
|
+
/** Every query-string parameter on the request, for the handful of commands with additional command-
|
|
36
|
+
* specific query parameters beyond the common four already broken out above. */
|
|
37
|
+
readonly query: Record<string, string | string[]>;
|
|
38
|
+
/** The decoded WBXML request body, or `undefined` for a command sent with an empty body (legal for a few
|
|
39
|
+
* commands, e.g. a bare `GetItemEstimate`-less `Ping` continuation). */
|
|
40
|
+
readonly request?: WbxmlElement;
|
|
41
|
+
/** The raw underlying HTTP request, for the rare handler that needs something this context doesn't
|
|
42
|
+
* already surface (e.g. a header). */
|
|
43
|
+
readonly req: HttpRequest;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* One EAS protocol command (`Provision`, `FolderSync`, `Sync`, ...). `BaseEasRoute` builds one instance of
|
|
47
|
+
* each registered handler class in its own `@Init` (via `ObjectFactory`, so a handler can `@Inject` its own
|
|
48
|
+
* dependencies exactly like any other DI-managed class in this library) and dispatches to the one whose
|
|
49
|
+
* `command` matches the request's `?Cmd=` value.
|
|
50
|
+
*
|
|
51
|
+
* @author Jean-Philippe Steinmetz
|
|
52
|
+
*/
|
|
53
|
+
export interface EasCommandHandler {
|
|
54
|
+
/** The exact `?Cmd=` value this handler answers to (e.g. `"FolderSync"`). */
|
|
55
|
+
readonly command: string;
|
|
56
|
+
/** Processes the command and returns the WBXML element tree to send back as the response body, or
|
|
57
|
+
* `undefined` for a command whose successful response is legitimately empty. */
|
|
58
|
+
handle(ctx: EasCommandContext): Promise<WbxmlElement | undefined>;
|
|
59
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { RecoverableBaseEntity, RepoUtils } from "@rapidrest/service-core";
|
|
2
|
+
import type { DeviceSyncState } from "./models/DeviceSyncState.js";
|
|
3
|
+
/**
|
|
4
|
+
* A parsed EAS `SyncKey`. The wire value is opaque to the client per spec, so this library encodes it as
|
|
5
|
+
* `"<generation>:<watermarkIso>"` (e.g. `"3:2026-09-04T12:00:00.000Z"`) - a monotonic generation counter (bumped
|
|
6
|
+
* once per successful sync round, satisfying the spec's "the server MUST return a different SyncKey every
|
|
7
|
+
* time" requirement) paired with the `dateModified` watermark that generation was issued at.
|
|
8
|
+
*/
|
|
9
|
+
export interface SyncKey {
|
|
10
|
+
generation: number;
|
|
11
|
+
watermark: Date;
|
|
12
|
+
}
|
|
13
|
+
/** Formats a `SyncKey` back into its wire string form. */
|
|
14
|
+
export declare function formatSyncKey(key: SyncKey): string;
|
|
15
|
+
/** Parses a stored/previously-issued `SyncKey` string. Returns `undefined` for a malformed value - callers
|
|
16
|
+
* only ever parse a key this library itself minted and stored (never a raw, unvalidated client value; see
|
|
17
|
+
* `resolveSyncKey`'s doc comment), so `undefined` here signals corrupted persisted state, not client input. */
|
|
18
|
+
export declare function parseSyncKey(value: string): SyncKey | undefined;
|
|
19
|
+
export type SyncKeyResolution = {
|
|
20
|
+
kind: "initial";
|
|
21
|
+
} | {
|
|
22
|
+
kind: "valid";
|
|
23
|
+
key: SyncKey;
|
|
24
|
+
} | {
|
|
25
|
+
kind: "invalid";
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Resolves an incoming client `SyncKey` string against the value this library itself previously issued and
|
|
29
|
+
* stored (`storedValue`, e.g. `DeviceSyncState.folderSyncKeys[folderUid]`) — deliberately a plain string
|
|
30
|
+
* equality check, not a re-parse-and-compare of the client's value, so a client that echoes back anything
|
|
31
|
+
* other than the exact opaque string it was handed is treated as `"invalid"` even if it happens to parse.
|
|
32
|
+
*
|
|
33
|
+
* - `"0"` (or empty/missing) from the client is always `"initial"` regardless of `storedValue` — an EAS
|
|
34
|
+
* client legitimately sends this to (re)start a collection from scratch (first-ever sync, or recovering
|
|
35
|
+
* from an `"invalid"` response elsewhere), and the spec requires the server honor it unconditionally.
|
|
36
|
+
* - Otherwise, a match against `storedValue` is `"valid"`; anything else (including `storedValue` being
|
|
37
|
+
* unset, i.e. the server has no record of ever issuing a key for this scope) is `"invalid"` — forcing the
|
|
38
|
+
* client back to `"0"`, per spec, rather than guessing at recovery.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveSyncKey(clientValue: string | undefined, storedValue: string | undefined): SyncKeyResolution;
|
|
41
|
+
/**
|
|
42
|
+
* Applies `patch` to `deviceSyncState` and persists it - the one correct way any EAS command handler (or
|
|
43
|
+
* `BaseEasRoute` itself) should ever write to a `DeviceSyncState`, replacing the inline
|
|
44
|
+
* `deviceSyncStateRepo.update(...)` calls `ProvisionCommand`/`FolderSyncCommand`/`SyncCommand` used to each
|
|
45
|
+
* repeat individually.
|
|
46
|
+
*
|
|
47
|
+
* This exists because `RepoUtils.update()` does **not** mutate the `existing` object passed to it - confirmed
|
|
48
|
+
* by reading its implementation directly - it only returns a freshly-fetched instance reflecting the write,
|
|
49
|
+
* leaving the caller's in-memory copy (including its optimistic-concurrency `version`) stale. A single write
|
|
50
|
+
* per request is harmless (nothing re-reads the stale copy), but a *second* write to the same `deviceSyncState`
|
|
51
|
+
* later in the same request - e.g. `BaseEasRoute.dispatch()`'s own trailing `lastSyncAt` update, or a second
|
|
52
|
+
* collection's SyncKey in a multi-collection `Sync` - would build its own patch's `version` off that stale
|
|
53
|
+
* value. `RepoUtils.update()` filters its underlying write by exactly that `version`, so the write silently
|
|
54
|
+
* matches zero rows instead of throwing - a silent no-op, not a visible error. Copying `update()`'s own
|
|
55
|
+
* returned result back onto `deviceSyncState` (last line below) keeps every field, including `version`, current
|
|
56
|
+
* for whatever writes this same request still has left to make.
|
|
57
|
+
*/
|
|
58
|
+
export declare function persistDeviceSyncState(deviceSyncState: DeviceSyncState, deviceSyncStateRepo: RepoUtils<any>, patch: Record<string, unknown>): Promise<void>;
|
|
59
|
+
/** One page of enumerated changes for a `RecoverableBaseEntity` collection scoped by a single field (e.g.
|
|
60
|
+
* `folderUid` for `Message`/`CalendarEvent`/.../`mailboxUid` for `Folder`), since the last sync at `watermark`. */
|
|
61
|
+
export interface ChangeSet<T extends RecoverableBaseEntity> {
|
|
62
|
+
adds: T[];
|
|
63
|
+
changes: T[];
|
|
64
|
+
deletes: T[];
|
|
65
|
+
/** The new watermark to persist as this generation's cursor - the latest `dateModified` actually included
|
|
66
|
+
* in this page, never simply "now" (which would silently skip any row modified after this page was read
|
|
67
|
+
* but before the caller finishes processing it). Equal to the input `watermark` when nothing changed. */
|
|
68
|
+
newWatermark: Date;
|
|
69
|
+
/** `true` when more changed rows exist beyond `windowSize` - the caller should set the response's
|
|
70
|
+
* `MoreAvailable` flag so the client immediately re-syncs for the rest, rather than waiting a full poll
|
|
71
|
+
* interval. */
|
|
72
|
+
moreAvailable: boolean;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Enumerates `Add`/`Change`/`Delete`s for one scoped collection since `watermark`, for `FolderSyncCommand` and
|
|
76
|
+
* (eventually) `SyncCommand`'s shared cursor mechanism. Requires `T` to be `RecoverableBaseEntity` (soft
|
|
77
|
+
* delete) — see `RecoverableRepoUtils`'s own doc comment for why a plain hard-deleted entity can't support
|
|
78
|
+
* this at all (nothing left to enumerate once a row is actually gone).
|
|
79
|
+
*
|
|
80
|
+
* `RepoUtils.find()` does **not** support an `includeDeleted` option - confirmed by reading its source: unlike
|
|
81
|
+
* `count()`/`findOne()`, it never strips `ModelUtils.buildSearchQuery()`'s default `deleted: false` exclusion
|
|
82
|
+
* back out. Rather than the (harmless-looking but silently no-op) `includeDeleted: true` this function used to
|
|
83
|
+
* pass, deleted rows are fetched via a **second** `find()` call with an explicit, literal `deleted: true` in
|
|
84
|
+
* the query object itself - `buildSearchQuery()`'s exclusion only applies when the caller's query has no
|
|
85
|
+
* `"deleted"` key at all, so supplying one directly (rather than relying on an option `find()` doesn't honor)
|
|
86
|
+
* reliably selects exactly the soft-deleted rows instead. The two result sets are then merged and re-windowed
|
|
87
|
+
* together so `windowSize`/`MoreAvailable` still describe the combined stream, not each half independently.
|
|
88
|
+
*/
|
|
89
|
+
export declare function computeChanges<T extends RecoverableBaseEntity>(repo: RepoUtils<T>, scopeField: string, scopeUid: string, watermark: Date, windowSize: number): Promise<ChangeSet<T>>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type WbxmlElement } from "../codec/WbxmlElement.js";
|
|
2
|
+
import type { EasCollectionSyncAdapter } from "./EasCollectionSyncAdapter.js";
|
|
3
|
+
import { type CalendarEvent } from "@rapidmx/restapi";
|
|
4
|
+
/**
|
|
5
|
+
* Maps `CalendarEvent` to/from the EAS `Sync` `Calendar` collection class (MS-ASCAL).
|
|
6
|
+
*
|
|
7
|
+
* **Pragmatic subset, deliberately not the full MS-ASCAL semantics**:
|
|
8
|
+
* - `TimeZone` is not emitted - the real element is a base64-encoded binary Win32 `TIME_ZONE_INFORMATION`
|
|
9
|
+
* structure, not a plain IANA string; `CalendarEvent.timezone` (an IANA identifier) can't be losslessly
|
|
10
|
+
* re-encoded into that format without a full IANA-to-Windows zone mapping table, and a real device would
|
|
11
|
+
* rather see no `TimeZone` element (falling back to its own default) than a malformed one.
|
|
12
|
+
* - `Sensitivity` is always reported `0` (Normal) - this library's `CalendarEvent` has no privacy dimension of
|
|
13
|
+
* its own to source a real value from.
|
|
14
|
+
* - Recurrence patterns keyed by an ordinal weekday (MS-ASCAL `Type` 3/6, e.g. "the 2nd Tuesday of the month")
|
|
15
|
+
* are not emitted - see `RECURRENCE_TYPE_CODES`'s own doc comment.
|
|
16
|
+
* - Recurrence exceptions (individually modified/cancelled occurrences of a recurring series) are not synced -
|
|
17
|
+
* deferred, matching this library's "pragmatic subset" precedent elsewhere (e.g. `FolderSyncCommand`'s SyncKey
|
|
18
|
+
* replay-protection gap).
|
|
19
|
+
*
|
|
20
|
+
* @author Jean-Philippe Steinmetz
|
|
21
|
+
*/
|
|
22
|
+
export declare class CalendarSyncAdapter implements EasCollectionSyncAdapter<CalendarEvent> {
|
|
23
|
+
readonly collectionClass = "Calendar";
|
|
24
|
+
toApplicationData(event: CalendarEvent): WbxmlElement;
|
|
25
|
+
private recurrenceElement;
|
|
26
|
+
/**
|
|
27
|
+
* Reverse of `toApplicationData`. `timezone`/`status`/`sequence`/`icalUid` have no wire representation at
|
|
28
|
+
* all (see this class's own "pragmatic subset" doc comment for `timezone`; the other three are purely
|
|
29
|
+
* server-managed identifiers/state a client was never sent in the first place) and are never included in
|
|
30
|
+
* the returned partial - `newEntityDefaults()` below supplies `icalUid`/`sequence` for a brand new event
|
|
31
|
+
* (`status`/`timezone` are left at the model's own constructor defaults), and `applyChange` leaves all four
|
|
32
|
+
* untouched by construction (merging onto `existing`).
|
|
33
|
+
*
|
|
34
|
+
* `OrganizerEmail` is required for a new event (there is nowhere else to default it from - this adapter
|
|
35
|
+
* has no mailbox context of its own) - a real calendar client always sends it regardless, since it already
|
|
36
|
+
* knows its own account's address. Omitting it on an `Add` throws, which `SyncCommand.applyAdd` turns into
|
|
37
|
+
* Status `6` ("client has sent a malformed or invalid item"), the spec's own designated code for exactly
|
|
38
|
+
* this case.
|
|
39
|
+
*
|
|
40
|
+
* `Attendees`/`Recurrence` are ghosted as a whole element, like `ContactsSyncAdapter`'s arrays: present at
|
|
41
|
+
* all -> rebuilt entirely from what's there; absent -> left untouched on a `Change`.
|
|
42
|
+
*/
|
|
43
|
+
fromApplicationData(el: WbxmlElement): Partial<CalendarEvent>;
|
|
44
|
+
/** `icalUid`/`sequence` have no wire representation on `Add` (see `fromApplicationData`'s own doc comment)
|
|
45
|
+
* - without this, every Sync-created event would fall back to `CalendarEventMongo`/`CalendarEventSQL`'s own
|
|
46
|
+
* constructor default of `icalUid: ""`, violating RFC 5545's uniqueness expectation for `UID`. Mirrors
|
|
47
|
+
* MAPI's identical `RopSaveChangesMessageHandler` pattern (`${crypto.randomUUID()}@mapi`), `@eas` suffix
|
|
48
|
+
* instead. */
|
|
49
|
+
newEntityDefaults(): Partial<CalendarEvent>;
|
|
50
|
+
private attendeeFromElement;
|
|
51
|
+
private recurrenceRuleFromElement;
|
|
52
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type WbxmlElement } from "../codec/WbxmlElement.js";
|
|
2
|
+
import type { EasCollectionSyncAdapter } from "./EasCollectionSyncAdapter.js";
|
|
3
|
+
import { type Contact } from "@rapidmx/restapi";
|
|
4
|
+
/**
|
|
5
|
+
* Maps `Contact` to/from the EAS `Sync` `Contacts` collection class (MS-ASCONTACTS/MS-ASCNTC2). Contacts are
|
|
6
|
+
* also this library's GAL source (see the architecture note on `Contact` itself), but that's `SearchCommand`'s
|
|
7
|
+
* concern, not this adapter's.
|
|
8
|
+
*
|
|
9
|
+
* @author Jean-Philippe Steinmetz
|
|
10
|
+
*/
|
|
11
|
+
export declare class ContactsSyncAdapter implements EasCollectionSyncAdapter<Contact> {
|
|
12
|
+
readonly collectionClass = "Contacts";
|
|
13
|
+
toApplicationData(contact: Contact): WbxmlElement;
|
|
14
|
+
private addressElements;
|
|
15
|
+
/**
|
|
16
|
+
* Reverse of `toApplicationData`. Scalar fields (`FileAs`/`FirstName`/.../`JobTitle`, and the `Body`
|
|
17
|
+
* `notes`) are properly ghosted - a field's own tag missing from `el` leaves that `Contact` field
|
|
18
|
+
* untouched. `emails`/`phones`/`addresses` are ghosted only as a **whole group**, not per slot: if *none*
|
|
19
|
+
* of a group's tags are present the group is left untouched, but if *any* one is, the entire group is
|
|
20
|
+
* rebuilt from just what's present in `el` (a real client's own Contacts edit UI typically resends every
|
|
21
|
+
* field it manages anyway, so this only under-preserves data for a client that deliberately sends a
|
|
22
|
+
* single-slot partial update within one of these groups - a documented simplification, not silent data
|
|
23
|
+
* loss for the common case). Emails lose their original `type` on any round trip through a `Change`
|
|
24
|
+
* (rebuilt as `ContactAddressKind.OTHER`) since EAS's own `Email1/2/3Address` tags carry no kind at all,
|
|
25
|
+
* matching `toApplicationData`'s own already-documented encode-side loss of the same information.
|
|
26
|
+
* `categories` is ghosted as its own whole group (same rule as `emails`/`phones`/`addresses`): an absent
|
|
27
|
+
* `Categories` element leaves `Contact.categories` untouched, while a present one - even `<Categories/>`
|
|
28
|
+
* with no `Category` children - rebuilds it from scratch (an empty array clears it).
|
|
29
|
+
*/
|
|
30
|
+
fromApplicationData(el: WbxmlElement): Partial<Contact>;
|
|
31
|
+
private addressTags;
|
|
32
|
+
private parseAddress;
|
|
33
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { RecoverableBaseEntity } from "@rapidrest/service-core";
|
|
2
|
+
import type { Mailbox } from "@rapidmx/restapi";
|
|
3
|
+
import type { WbxmlElement } from "../codec/WbxmlElement.js";
|
|
4
|
+
/**
|
|
5
|
+
* Maps one application entity type (`Message`/`Contact`/`CalendarEvent`/`Task`) to and from the EAS `Sync`
|
|
6
|
+
* command's per-collection wire representation. `SyncCommand` itself only knows the generic Add/Change/Delete
|
|
7
|
+
* cursor mechanics (shared with `FolderSyncCommand` via `EasSyncKeyUtils`) - everything entity-specific (which
|
|
8
|
+
* fields go in `ApplicationData`, on which code pages) lives in one adapter per collection type, keyed by the
|
|
9
|
+
* MS-ASCMD `Class` value (`"Email"`, `"Contacts"`, `"Calendar"`, `"Tasks"`) it answers to.
|
|
10
|
+
*
|
|
11
|
+
* `fromApplicationData` is deliberately optional: `SyncCommand` uses its absence as the capability check,
|
|
12
|
+
* answering Status `6` for `Add`/`Change` on a collection whose adapter doesn't implement it, rather than
|
|
13
|
+
* needing a separate flag that could drift out of sync with which adapters actually implement it. Every
|
|
14
|
+
* adapter today (`Email`, `Contacts`, `Calendar`, `Tasks`) implements it.
|
|
15
|
+
*
|
|
16
|
+
* @author Jean-Philippe Steinmetz
|
|
17
|
+
*/
|
|
18
|
+
export interface EasCollectionSyncAdapter<T extends RecoverableBaseEntity> {
|
|
19
|
+
/** The MS-ASCMD `Class` value this adapter handles, e.g. `"Email"`. */
|
|
20
|
+
readonly collectionClass: string;
|
|
21
|
+
/** Builds the `<ApplicationData>` element for one `Add`/`Change` command reporting `item`. May return a
|
|
22
|
+
* `Promise` - `EmailSyncAdapter` needs this to resolve `Message.labelUids` against the `Label` repo before
|
|
23
|
+
* rendering `Categories`; every other adapter today returns a plain `WbxmlElement`, which callers `await`
|
|
24
|
+
* through unchanged (the same optional-async shape `fromApplicationData` already established below). */
|
|
25
|
+
toApplicationData(item: T): WbxmlElement | Promise<WbxmlElement>;
|
|
26
|
+
/**
|
|
27
|
+
* Parses one client-originated `Add`/`Change` command's `<ApplicationData>` element (`el`) into a partial
|
|
28
|
+
* entity update. Only fields actually present in `el` are included in the result - an omitted field means
|
|
29
|
+
* "unchanged" (MS-ASCMD's own "ghosted property" rule - see `SyncCommand`'s doc comment), never "clear this
|
|
30
|
+
* field" - which is what lets the same method serve both `Add` (the partial is merged onto a fresh
|
|
31
|
+
* `{mailboxUid, folderUid}` baseline) and `Change` (the partial is merged onto `existing`).
|
|
32
|
+
*
|
|
33
|
+
* May return a `Promise` - `EmailSyncAdapter` needs this for a Draft `Add`/`Change`'s body, which must be
|
|
34
|
+
* written to `BlobStore` before the resulting `bodyBlobKey` is known; every other adapter today returns a
|
|
35
|
+
* plain object, which `await`s through unchanged.
|
|
36
|
+
*
|
|
37
|
+
* @param el The command's `<ApplicationData>` element.
|
|
38
|
+
* @param existing The item being changed, for a `Change` command; `undefined` for `Add`. Adapters that
|
|
39
|
+
* need to know the item's current field values to correctly interpret a partial update - `EmailSyncAdapter`
|
|
40
|
+
* uses this to reuse an existing Draft's `bodyBlobKey` on `Change` rather than minting a new one.
|
|
41
|
+
*/
|
|
42
|
+
fromApplicationData?(el: WbxmlElement, existing?: T): Partial<T> | Promise<Partial<T>>;
|
|
43
|
+
/**
|
|
44
|
+
* Supplies default field values for a brand-new entity created via a client-originated `Add`, applied
|
|
45
|
+
* *before* `fromApplicationData`'s own partial is merged on top (so anything the client actually sent
|
|
46
|
+
* always wins). For defaults a fresh entity needs regardless of what the client sent - `CalendarSyncAdapter`
|
|
47
|
+
* uses this for `icalUid`/`sequence`, since EAS's own `Add` command has no wire representation for either
|
|
48
|
+
* (a device doesn't know or send an iCalendar UID) but a stored default of `""` for every Sync-created
|
|
49
|
+
* event (this model's own fallback, see `CalendarEventMongo`'s constructor) would violate RFC 5545's own
|
|
50
|
+
* uniqueness expectation for `UID`. `EmailSyncAdapter` uses `mailbox` to populate a new Draft's `from`.
|
|
51
|
+
* Optional; only implemented where a collection actually needs it - most adapters have no such gap (and
|
|
52
|
+
* ignore the `mailbox` parameter entirely).
|
|
53
|
+
*/
|
|
54
|
+
newEntityDefaults?(mailbox: Mailbox): Partial<T>;
|
|
55
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type WbxmlElement } from "../codec/WbxmlElement.js";
|
|
2
|
+
import type { EasCollectionSyncAdapter } from "./EasCollectionSyncAdapter.js";
|
|
3
|
+
import { type Mailbox, type Message } from "@rapidmx/restapi";
|
|
4
|
+
/**
|
|
5
|
+
* Maps `Message` to/from the EAS `Sync` `Email` collection class (MS-ASEMAIL). Only a plain-text preview of the
|
|
6
|
+
* body is included here (`Message.bodyPreview`, always already loaded on the entity, `Truncated: 1`) rather
|
|
7
|
+
* than the full sanitized HTML body from the `BlobStore` - a real device fetches the full body on demand via
|
|
8
|
+
* `ItemOperations`' `Fetch` (see the architecture plan's command table), the same two-step "list, then fetch
|
|
9
|
+
* body" flow every real EAS client already implements for exactly this reason (bodies can be large; a sync
|
|
10
|
+
* window's Add/Change list shouldn't have to pull every one of them from blob storage up front).
|
|
11
|
+
*
|
|
12
|
+
* Also handles client-originated `Add`/`Change` for Drafts (`SyncCommand`'s own doc comment covers why this is
|
|
13
|
+
* the only `Email` write EAS itself allows) - a plain-text-only pragmatic subset: no HTML body, no attachments
|
|
14
|
+
* (mirrors `ComposeMailCommand`'s own already-documented attachment gap). `To`/`Cc`/`Bcc` (the latter MS-ASEMAIL2's
|
|
15
|
+
* own `Bcc` tag) are ghosted independently per recipient type, not as one combined group - a `Change` touching
|
|
16
|
+
* only one of them leaves the others untouched, carried over from `existing.recipients`.
|
|
17
|
+
*
|
|
18
|
+
* Emits MS-ASEMAIL2's `Email2:ConversationId` (read-only - no `fromApplicationData` handling, since EAS itself
|
|
19
|
+
* never lets a client set it) whenever `Message.conversationId` is populated, so a device's threaded-view UI can
|
|
20
|
+
* group messages the same way `BaseMessageRoute.conversations()` does server-side. See `encodeConversationId`'s
|
|
21
|
+
* own doc comment for the wire encoding, and `ItemOperationsCommand`'s `Move` handling for the one place this
|
|
22
|
+
* gets decoded back.
|
|
23
|
+
*
|
|
24
|
+
* Emits MS-ASEMAIL's `Categories`/`Category` (also read-only) from `Message.labelUids`, resolved against the
|
|
25
|
+
* `Label` repo to real display names - a stale `labelUids` entry (the label was since deleted) is silently
|
|
26
|
+
* dropped rather than surfacing as an error, the same "tolerate a dangling reference" stance `SearchCommand`
|
|
27
|
+
* already takes for a stale search-index entry. Deliberately not writable: unlike `Contact.categories` (a
|
|
28
|
+
* plain free-form string array with no separate entity behind it), a `Label` is a real mailbox-scoped entity
|
|
29
|
+
* referenced by uid - a write path would need to resolve category name strings back to `Label`s and create new
|
|
30
|
+
* ones on the fly for names that don't exist yet, real added scope this pragmatic subset defers. One `find()`
|
|
31
|
+
* per message that actually has labels (most won't, since labels are opt-in) - not batched across a whole Sync
|
|
32
|
+
* page/search result set, a documented, modest N+1 tradeoff rather than widening this adapter's own interface
|
|
33
|
+
* further to let a caller pre-resolve names for a whole batch.
|
|
34
|
+
*
|
|
35
|
+
* `labelClass` is supplied by the Mongo/SQL concrete subclasses.
|
|
36
|
+
*
|
|
37
|
+
* @author Jean-Philippe Steinmetz
|
|
38
|
+
*/
|
|
39
|
+
export declare abstract class EmailSyncAdapter implements EasCollectionSyncAdapter<Message> {
|
|
40
|
+
readonly collectionClass = "Email";
|
|
41
|
+
protected abstract labelClass: any;
|
|
42
|
+
private _objectFactory?;
|
|
43
|
+
private labelRepo?;
|
|
44
|
+
private blobStore?;
|
|
45
|
+
init(): Promise<void>;
|
|
46
|
+
toApplicationData(message: Message): Promise<WbxmlElement>;
|
|
47
|
+
private resolveLabelNames;
|
|
48
|
+
/**
|
|
49
|
+
* `Message.bodyBlobKey` is documented (see the `Message` interface itself) as holding raw MIME "unmodified
|
|
50
|
+
* from ingestion/send" - `ItemOperationsCommand.fetchMessage` parses it with `simpleParser` unconditionally
|
|
51
|
+
* for every message, Draft or not. A Draft's plain-text body is therefore wrapped in a minimal valid
|
|
52
|
+
* RFC 5322 message here (via `buildPlainTextMime`) rather than stored as bare text, so that contract holds
|
|
53
|
+
* for every consumer, not just this write path - a Draft created/edited via `Sync` must `Fetch` correctly
|
|
54
|
+
* the same way any other message does.
|
|
55
|
+
*/
|
|
56
|
+
fromApplicationData(el: WbxmlElement, existing?: Message): Promise<Partial<Message>>;
|
|
57
|
+
/** Defaults for a brand-new Draft created via a client-originated `Add` - `from` is the caller's own
|
|
58
|
+
* mailbox address, per `ComposeMailCommand`'s identical `{ address, type: RecipientType.TO }` shape
|
|
59
|
+
* convention for a `from` field (the `Recipient` struct's `type` is only meaningful for real recipients;
|
|
60
|
+
* it's reused here as a harmless placeholder). */
|
|
61
|
+
newEntityDefaults(mailbox: Mailbox): Partial<Message>;
|
|
62
|
+
}
|
|
63
|
+
/** Encodes `Message.conversationId` (an internal string uid) into the opaque binary blob MS-ASEMAIL2's
|
|
64
|
+
* `Email2:ConversationId` carries on the wire. The spec never mandates any particular binary format for this
|
|
65
|
+
* value - a real Exchange server mints a GUID, but a client only ever compares/echoes it byte-for-byte, never
|
|
66
|
+
* interprets it - so encoding the uid's own UTF-8 bytes directly (rather than hashing into a 16-byte GUID
|
|
67
|
+
* shape) is a valid, simpler choice that `decodeConversationId` can invert exactly, which `ItemOperationsCommand`
|
|
68
|
+
* relies on to resolve an `ItemOperations` `Move`'s `ConversationId` back into this same uid. */
|
|
69
|
+
export declare function encodeConversationId(conversationId: string): Buffer;
|
|
70
|
+
/** Reverse of `encodeConversationId`. */
|
|
71
|
+
export declare function decodeConversationId(opaque: Buffer): string;
|