@rapidmx/activesync 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +382 -0
- package/README.md +57 -0
- package/dist/lib/BaseDeviceSyncStateRoute.js +90 -0
- package/dist/lib/BaseDeviceSyncStateRoute.js.map +1 -0
- package/dist/lib/BaseEasRoute.js +213 -0
- package/dist/lib/BaseEasRoute.js.map +1 -0
- package/dist/lib/CompactDateTime.js +43 -0
- package/dist/lib/CompactDateTime.js.map +1 -0
- package/dist/lib/EasCommandHandler.js +2 -0
- package/dist/lib/EasCommandHandler.js.map +1 -0
- package/dist/lib/EasSyncKeyUtils.js +119 -0
- package/dist/lib/EasSyncKeyUtils.js.map +1 -0
- package/dist/lib/adapters/CalendarSyncAdapter.js +255 -0
- package/dist/lib/adapters/CalendarSyncAdapter.js.map +1 -0
- package/dist/lib/adapters/ContactsSyncAdapter.js +164 -0
- package/dist/lib/adapters/ContactsSyncAdapter.js.map +1 -0
- package/dist/lib/adapters/EasCollectionSyncAdapter.js +2 -0
- package/dist/lib/adapters/EasCollectionSyncAdapter.js.map +1 -0
- package/dist/lib/adapters/EmailSyncAdapter.js +230 -0
- package/dist/lib/adapters/EmailSyncAdapter.js.map +1 -0
- package/dist/lib/adapters/TasksSyncAdapter.js +100 -0
- package/dist/lib/adapters/TasksSyncAdapter.js.map +1 -0
- package/dist/lib/codec/WbxmlCodePages.js +706 -0
- package/dist/lib/codec/WbxmlCodePages.js.map +1 -0
- package/dist/lib/codec/WbxmlDecoder.js +154 -0
- package/dist/lib/codec/WbxmlDecoder.js.map +1 -0
- package/dist/lib/codec/WbxmlElement.js +31 -0
- package/dist/lib/codec/WbxmlElement.js.map +1 -0
- package/dist/lib/codec/WbxmlEncoder.js +88 -0
- package/dist/lib/codec/WbxmlEncoder.js.map +1 -0
- package/dist/lib/commands/ComposeMailCommand.js +187 -0
- package/dist/lib/commands/ComposeMailCommand.js.map +1 -0
- package/dist/lib/commands/FolderSyncCommand.js +150 -0
- package/dist/lib/commands/FolderSyncCommand.js.map +1 -0
- package/dist/lib/commands/GetItemEstimateCommand.js +149 -0
- package/dist/lib/commands/GetItemEstimateCommand.js.map +1 -0
- package/dist/lib/commands/ItemOperationsCommand.js +337 -0
- package/dist/lib/commands/ItemOperationsCommand.js.map +1 -0
- package/dist/lib/commands/MeetingResponseCommand.js +120 -0
- package/dist/lib/commands/MeetingResponseCommand.js.map +1 -0
- package/dist/lib/commands/MoveItemsCommand.js +103 -0
- package/dist/lib/commands/MoveItemsCommand.js.map +1 -0
- package/dist/lib/commands/PingCommand.js +145 -0
- package/dist/lib/commands/PingCommand.js.map +1 -0
- package/dist/lib/commands/ProvisionCommand.js +164 -0
- package/dist/lib/commands/ProvisionCommand.js.map +1 -0
- package/dist/lib/commands/ResolveRecipientsCommand.js +124 -0
- package/dist/lib/commands/ResolveRecipientsCommand.js.map +1 -0
- package/dist/lib/commands/SearchCommand.js +137 -0
- package/dist/lib/commands/SearchCommand.js.map +1 -0
- package/dist/lib/commands/SendMailCommand.js +18 -0
- package/dist/lib/commands/SendMailCommand.js.map +1 -0
- package/dist/lib/commands/SettingsCommand.js +138 -0
- package/dist/lib/commands/SettingsCommand.js.map +1 -0
- package/dist/lib/commands/SmartForwardCommand.js +22 -0
- package/dist/lib/commands/SmartForwardCommand.js.map +1 -0
- package/dist/lib/commands/SmartReplyCommand.js +22 -0
- package/dist/lib/commands/SmartReplyCommand.js.map +1 -0
- package/dist/lib/commands/SyncCommand.js +406 -0
- package/dist/lib/commands/SyncCommand.js.map +1 -0
- package/dist/lib/commands/mongo/FolderSyncCommandMongo.js +16 -0
- package/dist/lib/commands/mongo/FolderSyncCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/GetItemEstimateCommandMongo.js +21 -0
- package/dist/lib/commands/mongo/GetItemEstimateCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/ItemOperationsCommandMongo.js +18 -0
- package/dist/lib/commands/mongo/ItemOperationsCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/MeetingResponseCommandMongo.js +17 -0
- package/dist/lib/commands/mongo/MeetingResponseCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/MoveItemsCommandMongo.js +17 -0
- package/dist/lib/commands/mongo/MoveItemsCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/ResolveRecipientsCommandMongo.js +20 -0
- package/dist/lib/commands/mongo/ResolveRecipientsCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/SearchCommandMongo.js +20 -0
- package/dist/lib/commands/mongo/SearchCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/SendMailCommandMongo.js +17 -0
- package/dist/lib/commands/mongo/SendMailCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/SettingsCommandMongo.js +16 -0
- package/dist/lib/commands/mongo/SettingsCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/SmartForwardCommandMongo.js +17 -0
- package/dist/lib/commands/mongo/SmartForwardCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/SmartReplyCommandMongo.js +17 -0
- package/dist/lib/commands/mongo/SmartReplyCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/SyncCommandMongo.js +26 -0
- package/dist/lib/commands/mongo/SyncCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/index.js +14 -0
- package/dist/lib/commands/mongo/index.js.map +1 -0
- package/dist/lib/commands/sql/FolderSyncCommandSQL.js +16 -0
- package/dist/lib/commands/sql/FolderSyncCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/GetItemEstimateCommandSQL.js +21 -0
- package/dist/lib/commands/sql/GetItemEstimateCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/ItemOperationsCommandSQL.js +18 -0
- package/dist/lib/commands/sql/ItemOperationsCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/MeetingResponseCommandSQL.js +17 -0
- package/dist/lib/commands/sql/MeetingResponseCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/MoveItemsCommandSQL.js +17 -0
- package/dist/lib/commands/sql/MoveItemsCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/ResolveRecipientsCommandSQL.js +20 -0
- package/dist/lib/commands/sql/ResolveRecipientsCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/SearchCommandSQL.js +20 -0
- package/dist/lib/commands/sql/SearchCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/SendMailCommandSQL.js +17 -0
- package/dist/lib/commands/sql/SendMailCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/SettingsCommandSQL.js +16 -0
- package/dist/lib/commands/sql/SettingsCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/SmartForwardCommandSQL.js +17 -0
- package/dist/lib/commands/sql/SmartForwardCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/SmartReplyCommandSQL.js +17 -0
- package/dist/lib/commands/sql/SmartReplyCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/SyncCommandSQL.js +26 -0
- package/dist/lib/commands/sql/SyncCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/index.js +14 -0
- package/dist/lib/commands/sql/index.js.map +1 -0
- package/dist/lib/index.js +56 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/mongo/DeviceSyncStateRouteMongo.js +16 -0
- package/dist/lib/mongo/DeviceSyncStateRouteMongo.js.map +1 -0
- package/dist/lib/mongo/EasRouteMongo.js +51 -0
- package/dist/lib/mongo/EasRouteMongo.js.map +1 -0
- package/dist/lib/mongo/index.js +8 -0
- package/dist/lib/mongo/index.js.map +1 -0
- package/dist/lib/mongo.js +6 -0
- package/dist/lib/mongo.js.map +1 -0
- package/dist/lib/sql/DeviceSyncStateRouteSQL.js +16 -0
- package/dist/lib/sql/DeviceSyncStateRouteSQL.js.map +1 -0
- package/dist/lib/sql/EasRouteSQL.js +50 -0
- package/dist/lib/sql/EasRouteSQL.js.map +1 -0
- package/dist/lib/sql/index.js +8 -0
- package/dist/lib/sql/index.js.map +1 -0
- package/dist/lib/sql.js +6 -0
- package/dist/lib/sql.js.map +1 -0
- package/dist/types/BaseDeviceSyncStateRoute.d.ts +34 -0
- package/dist/types/BaseEasRoute.d.ts +64 -0
- package/dist/types/CompactDateTime.d.ts +22 -0
- package/dist/types/EasCommandHandler.d.ts +59 -0
- package/dist/types/EasSyncKeyUtils.d.ts +89 -0
- package/dist/types/adapters/CalendarSyncAdapter.d.ts +52 -0
- package/dist/types/adapters/ContactsSyncAdapter.d.ts +33 -0
- package/dist/types/adapters/EasCollectionSyncAdapter.d.ts +52 -0
- package/dist/types/adapters/EmailSyncAdapter.d.ts +53 -0
- package/dist/types/adapters/TasksSyncAdapter.d.ts +32 -0
- package/dist/types/codec/WbxmlCodePages.d.ts +45 -0
- package/dist/types/codec/WbxmlDecoder.d.ts +33 -0
- package/dist/types/codec/WbxmlElement.d.ts +43 -0
- package/dist/types/codec/WbxmlEncoder.d.ts +20 -0
- package/dist/types/commands/ComposeMailCommand.d.ts +50 -0
- package/dist/types/commands/FolderSyncCommand.d.ts +25 -0
- package/dist/types/commands/GetItemEstimateCommand.d.ts +43 -0
- package/dist/types/commands/ItemOperationsCommand.d.ts +72 -0
- package/dist/types/commands/MeetingResponseCommand.d.ts +32 -0
- package/dist/types/commands/MoveItemsCommand.d.ts +29 -0
- package/dist/types/commands/PingCommand.d.ts +33 -0
- package/dist/types/commands/ProvisionCommand.d.ts +54 -0
- package/dist/types/commands/ResolveRecipientsCommand.d.ts +34 -0
- package/dist/types/commands/SearchCommand.d.ts +39 -0
- package/dist/types/commands/SendMailCommand.d.ts +10 -0
- package/dist/types/commands/SettingsCommand.d.ts +37 -0
- package/dist/types/commands/SmartForwardCommand.d.ts +17 -0
- package/dist/types/commands/SmartReplyCommand.d.ts +17 -0
- package/dist/types/commands/SyncCommand.d.ts +100 -0
- package/dist/types/commands/mongo/FolderSyncCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/GetItemEstimateCommandMongo.d.ts +7 -0
- package/dist/types/commands/mongo/ItemOperationsCommandMongo.d.ts +9 -0
- package/dist/types/commands/mongo/MeetingResponseCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/MoveItemsCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/ResolveRecipientsCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/SearchCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/SendMailCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/SettingsCommandMongo.d.ts +7 -0
- package/dist/types/commands/mongo/SmartForwardCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/SmartReplyCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/SyncCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/index.d.ts +9 -0
- package/dist/types/commands/sql/FolderSyncCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/GetItemEstimateCommandSQL.d.ts +7 -0
- package/dist/types/commands/sql/ItemOperationsCommandSQL.d.ts +9 -0
- package/dist/types/commands/sql/MeetingResponseCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/MoveItemsCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/ResolveRecipientsCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/SearchCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/SendMailCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/SettingsCommandSQL.d.ts +7 -0
- package/dist/types/commands/sql/SmartForwardCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/SmartReplyCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/SyncCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/index.d.ts +9 -0
- package/dist/types/index.d.ts +51 -0
- package/dist/types/mongo/DeviceSyncStateRouteMongo.d.ts +8 -0
- package/dist/types/mongo/EasRouteMongo.d.ts +14 -0
- package/dist/types/mongo/index.d.ts +3 -0
- package/dist/types/mongo.d.ts +1 -0
- package/dist/types/sql/DeviceSyncStateRouteSQL.d.ts +8 -0
- package/dist/types/sql/EasRouteSQL.d.ts +13 -0
- package/dist/types/sql/index.d.ts +3 -0
- package/dist/types/sql.d.ts +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { ComposeMailCommand } from "./ComposeMailCommand.js";
|
|
6
|
+
/**
|
|
7
|
+
* Handles EAS `SendMail`: relays a freshly composed message with no referenced original - see
|
|
8
|
+
* `ComposeMailCommand`'s own doc comment for the full shared implementation.
|
|
9
|
+
*
|
|
10
|
+
* @author Jean-Philippe Steinmetz
|
|
11
|
+
*/
|
|
12
|
+
export class SendMailCommand extends ComposeMailCommand {
|
|
13
|
+
constructor() {
|
|
14
|
+
super(...arguments);
|
|
15
|
+
this.command = "SendMail";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=SendMailCommand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SendMailCommand.js","sourceRoot":"","sources":["../../../src/commands/SendMailCommand.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D;;;;;GAKG;AACH,MAAM,OAAgB,eAAgB,SAAQ,kBAAkB;IAAhE;;QACoB,YAAO,GAAG,UAAU,CAAC;IACzC,CAAC;CAAA"}
|
|
@@ -0,0 +1,138 @@
|
|
|
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 { Init } = ObjectDecorators;
|
|
19
|
+
/**
|
|
20
|
+
* Handles EAS `Settings`: the first-run/general-purpose device<->server settings exchange. This pragmatic
|
|
21
|
+
* subset supports the sub-elements every real client actually depends on:
|
|
22
|
+
*
|
|
23
|
+
* - `UserInformation`/`Get`: returns the mailbox's `primarySmtpAddress`/`aliasAddresses` as `EmailAddresses`.
|
|
24
|
+
* - `DeviceInformation`/`Set`: acknowledged with `Status 1` but not persisted anywhere - `DeviceSyncState` has
|
|
25
|
+
* no fields for a device's model/IMEI/OS/friendly name, and nothing else in this library currently consumes
|
|
26
|
+
* them. A real client only requires the acknowledgement to proceed past first-run setup, not that the values
|
|
27
|
+
* are retrievable later.
|
|
28
|
+
* - `Oof`/`Get` and `Oof`/`Set`: reads/writes `Mailbox.oofEnabled`/`oofMessage`/`oofStartTime`/`oofEndTime`.
|
|
29
|
+
* `StartTime`/`EndTime` use MS-ASDTYPE's plain `dateTime` type (`Date.prototype.toISOString()`), not Compact
|
|
30
|
+
* DateTime - confirmed against MS-ASSETTINGS directly, unlike `Calendar`/`Tasks`' timestamp fields (see
|
|
31
|
+
* `CompactDateTime.ts`'s own doc comment on that exact distinction). A single combined reply message is stored
|
|
32
|
+
* rather than the spec's three audience-specific `OofMessage` variants (internal/external-known/
|
|
33
|
+
* external-unknown) - matches this codebase's existing "one thing, not three" simplification precedent (e.g.
|
|
34
|
+
* `ContactsSyncAdapter`'s single-slot phone/email handling).
|
|
35
|
+
* - `RightsManagementInformation` is not implemented - deferred, matching this library's "pragmatic subset"
|
|
36
|
+
* precedent elsewhere (e.g. `ComposeMailCommand`'s own documented gaps).
|
|
37
|
+
*
|
|
38
|
+
* `mailboxClass` is supplied by the Mongo/SQL concrete subclasses.
|
|
39
|
+
*
|
|
40
|
+
* @author Jean-Philippe Steinmetz
|
|
41
|
+
*/
|
|
42
|
+
export class SettingsCommand {
|
|
43
|
+
constructor() {
|
|
44
|
+
this.command = "Settings";
|
|
45
|
+
}
|
|
46
|
+
async init() {
|
|
47
|
+
this.mailboxRepo = await this._objectFactory.newInstance(RepoUtils, {
|
|
48
|
+
name: this.mailboxClass.name,
|
|
49
|
+
args: [this.mailboxClass],
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async handle(ctx) {
|
|
53
|
+
if (!this.mailboxRepo) {
|
|
54
|
+
throw new ApiError(ApiErrors.INTERNAL_ERROR, 500, ApiErrorMessages.INTERNAL_ERROR);
|
|
55
|
+
}
|
|
56
|
+
const children = [textElement(WbxmlCodePage.Settings, "Status", "1")];
|
|
57
|
+
if (ctx.request && findChild(ctx.request, "DeviceInformation")) {
|
|
58
|
+
children.push(element(WbxmlCodePage.Settings, "DeviceInformation", [textElement(WbxmlCodePage.Settings, "Status", "1")]));
|
|
59
|
+
}
|
|
60
|
+
if (ctx.request && findChild(ctx.request, "UserInformation")) {
|
|
61
|
+
const mailbox = await this.mailboxRepo.findOne(ctx.mailboxUid, { ignoreACL: true });
|
|
62
|
+
if (!mailbox) {
|
|
63
|
+
throw new ApiError(ApiErrors.NOT_FOUND, 404, ApiErrorMessages.NOT_FOUND);
|
|
64
|
+
}
|
|
65
|
+
children.push(element(WbxmlCodePage.Settings, "UserInformation", [
|
|
66
|
+
textElement(WbxmlCodePage.Settings, "Status", "1"),
|
|
67
|
+
element(WbxmlCodePage.Settings, "EmailAddresses", [
|
|
68
|
+
textElement(WbxmlCodePage.Settings, "SmtpAddress", mailbox.primarySmtpAddress),
|
|
69
|
+
...mailbox.aliasAddresses.map((address) => textElement(WbxmlCodePage.Settings, "SmtpAddress", address)),
|
|
70
|
+
]),
|
|
71
|
+
]));
|
|
72
|
+
}
|
|
73
|
+
const oofEl = ctx.request ? findChild(ctx.request, "Oof") : undefined;
|
|
74
|
+
if (oofEl) {
|
|
75
|
+
const mailbox = await this.mailboxRepo.findOne(ctx.mailboxUid, { ignoreACL: true });
|
|
76
|
+
if (!mailbox) {
|
|
77
|
+
throw new ApiError(ApiErrors.NOT_FOUND, 404, ApiErrorMessages.NOT_FOUND);
|
|
78
|
+
}
|
|
79
|
+
const setEl = findChild(oofEl, "Set");
|
|
80
|
+
children.push(setEl ? await this.setOof(ctx, mailbox, setEl) : this.getOof(mailbox));
|
|
81
|
+
}
|
|
82
|
+
return element(WbxmlCodePage.Settings, "Settings", children);
|
|
83
|
+
}
|
|
84
|
+
/** `OofState`: `0`=disabled, `1`=enabled indefinitely, `2`=time-based (only while now is within
|
|
85
|
+
* `StartTime`/`EndTime`). */
|
|
86
|
+
getOof(mailbox) {
|
|
87
|
+
const timed = mailbox.oofEnabled && !!mailbox.oofStartTime && !!mailbox.oofEndTime;
|
|
88
|
+
return element(WbxmlCodePage.Settings, "Oof", [
|
|
89
|
+
textElement(WbxmlCodePage.Settings, "Status", "1"),
|
|
90
|
+
element(WbxmlCodePage.Settings, "Get", [
|
|
91
|
+
textElement(WbxmlCodePage.Settings, "OofState", mailbox.oofEnabled ? (timed ? "2" : "1") : "0"),
|
|
92
|
+
...(timed
|
|
93
|
+
? [
|
|
94
|
+
textElement(WbxmlCodePage.Settings, "StartTime", mailbox.oofStartTime.toISOString()),
|
|
95
|
+
textElement(WbxmlCodePage.Settings, "EndTime", mailbox.oofEndTime.toISOString()),
|
|
96
|
+
]
|
|
97
|
+
: []),
|
|
98
|
+
element(WbxmlCodePage.Settings, "OofMessage", [
|
|
99
|
+
element(WbxmlCodePage.Settings, "AppliesToInternal", []),
|
|
100
|
+
textElement(WbxmlCodePage.Settings, "Enabled", mailbox.oofEnabled ? "1" : "0"),
|
|
101
|
+
textElement(WbxmlCodePage.Settings, "ReplyMessage", mailbox.oofMessage),
|
|
102
|
+
textElement(WbxmlCodePage.Settings, "BodyType", "Text"),
|
|
103
|
+
]),
|
|
104
|
+
]),
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
async setOof(ctx, mailbox, setEl) {
|
|
108
|
+
const oofState = childText(setEl, "OofState") ?? "0";
|
|
109
|
+
const oofMessageEl = findChild(setEl, "OofMessage");
|
|
110
|
+
const replyMessage = oofMessageEl ? (childText(oofMessageEl, "ReplyMessage") ?? "") : "";
|
|
111
|
+
const startTime = childText(setEl, "StartTime");
|
|
112
|
+
const endTime = childText(setEl, "EndTime");
|
|
113
|
+
const timed = oofState === "2" && startTime !== undefined && endTime !== undefined;
|
|
114
|
+
// `null`, not `undefined`, to clear a previously-set window when switching away from time-based Oof:
|
|
115
|
+
// TypeORM's `UpdateQueryBuilder` silently drops any `undefined`-valued key from the generated SQL `SET`
|
|
116
|
+
// clause (confirmed by reading its source), so `undefined` here would leave a stale StartTime/EndTime
|
|
117
|
+
// in place on the SQL backend while correctly clearing it on Mongo - `null` clears it on both.
|
|
118
|
+
await this.mailboxRepo.update({
|
|
119
|
+
uid: mailbox.uid,
|
|
120
|
+
version: mailbox.version,
|
|
121
|
+
oofEnabled: oofState !== "0",
|
|
122
|
+
oofMessage: replyMessage,
|
|
123
|
+
oofStartTime: timed ? new Date(startTime) : null,
|
|
124
|
+
oofEndTime: timed ? new Date(endTime) : null,
|
|
125
|
+
}, mailbox, { ignoreACL: true, user: ctx.user });
|
|
126
|
+
return element(WbxmlCodePage.Settings, "Oof", [
|
|
127
|
+
textElement(WbxmlCodePage.Settings, "Status", "1"),
|
|
128
|
+
element(WbxmlCodePage.Settings, "Set", [textElement(WbxmlCodePage.Settings, "Status", "1")]),
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
__decorate([
|
|
133
|
+
Init,
|
|
134
|
+
__metadata("design:type", Function),
|
|
135
|
+
__metadata("design:paramtypes", []),
|
|
136
|
+
__metadata("design:returntype", Promise)
|
|
137
|
+
], SettingsCommand.prototype, "init", null);
|
|
138
|
+
//# sourceMappingURL=SettingsCommand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SettingsCommand.js","sourceRoot":"","sources":["../../../src/commands/SettingsCommand.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { ComposeMailCommand } from "./ComposeMailCommand.js";
|
|
6
|
+
/**
|
|
7
|
+
* Handles EAS `SmartForward`: relays a freshly composed message threaded to (and referencing) the original via
|
|
8
|
+
* `<Source>`, then flips the original's `Forwarded` flag - see `ComposeMailCommand`'s own doc comment for the
|
|
9
|
+
* full shared implementation and this pragmatic subset's scope.
|
|
10
|
+
*
|
|
11
|
+
* @author Jean-Philippe Steinmetz
|
|
12
|
+
*/
|
|
13
|
+
export class SmartForwardCommand extends ComposeMailCommand {
|
|
14
|
+
constructor() {
|
|
15
|
+
super(...arguments);
|
|
16
|
+
this.command = "SmartForward";
|
|
17
|
+
}
|
|
18
|
+
async markOriginal(ctx, original) {
|
|
19
|
+
await this.messageRepo.update({ uid: original.uid, version: original.version, flags: { ...original.flags, forwarded: true } }, original, { ignoreACL: true, user: ctx.user });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=SmartForwardCommand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SmartForwardCommand.js","sourceRoot":"","sources":["../../../src/commands/SmartForwardCommand.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAI7D;;;;;;GAMG;AACH,MAAM,OAAgB,mBAAoB,SAAQ,kBAAkB;IAApE;;QACoB,YAAO,GAAG,cAAc,CAAC;IAS7C,CAAC;IAPsB,KAAK,CAAC,YAAY,CAAC,GAAsB,EAAE,QAAoD;QAC9G,MAAM,IAAI,CAAC,WAAY,CAAC,MAAM,CAC1B,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,GAAG,QAAQ,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,EAAS,EACtG,QAAQ,EACR,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CACtC,CAAC;IACN,CAAC;CACJ"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { ComposeMailCommand } from "./ComposeMailCommand.js";
|
|
6
|
+
/**
|
|
7
|
+
* Handles EAS `SmartReply`: relays a freshly composed message threaded to (and referencing) the original via
|
|
8
|
+
* `<Source>`, then flips the original's `Answered` flag - see `ComposeMailCommand`'s own doc comment for the
|
|
9
|
+
* full shared implementation and this pragmatic subset's scope.
|
|
10
|
+
*
|
|
11
|
+
* @author Jean-Philippe Steinmetz
|
|
12
|
+
*/
|
|
13
|
+
export class SmartReplyCommand extends ComposeMailCommand {
|
|
14
|
+
constructor() {
|
|
15
|
+
super(...arguments);
|
|
16
|
+
this.command = "SmartReply";
|
|
17
|
+
}
|
|
18
|
+
async markOriginal(ctx, original) {
|
|
19
|
+
await this.messageRepo.update({ uid: original.uid, version: original.version, flags: { ...original.flags, answered: true } }, original, { ignoreACL: true, user: ctx.user });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=SmartReplyCommand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SmartReplyCommand.js","sourceRoot":"","sources":["../../../src/commands/SmartReplyCommand.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAI7D;;;;;;GAMG;AACH,MAAM,OAAgB,iBAAkB,SAAQ,kBAAkB;IAAlE;;QACoB,YAAO,GAAG,YAAY,CAAC;IAS3C,CAAC;IAPsB,KAAK,CAAC,YAAY,CAAC,GAAsB,EAAE,QAAoD;QAC9G,MAAM,IAAI,CAAC,WAAY,CAAC,MAAM,CAC1B,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,GAAG,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAS,EACrG,QAAQ,EACR,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CACtC,CAAC;IACN,CAAC;CACJ"}
|
|
@@ -0,0 +1,406 @@
|
|
|
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 { ACLAction, ACLUtils, ApiErrorMessages, ApiErrors, RepoUtils, } from "@rapidrest/service-core";
|
|
16
|
+
import { RecoverableRepoUtils } from "@rapidmx/restapi";
|
|
17
|
+
import { WbxmlCodePage } from "../codec/WbxmlCodePages.js";
|
|
18
|
+
import { childText, element, findChild, findChildren, textElement } from "../codec/WbxmlElement.js";
|
|
19
|
+
import { computeChanges, formatSyncKey, persistDeviceSyncState, resolveSyncKey } from "../EasSyncKeyUtils.js";
|
|
20
|
+
const { Config, Init, Inject } = ObjectDecorators;
|
|
21
|
+
/** Caps how many item changes are enumerated per `Sync` round - a real device-visible "MoreAvailable" trigger
|
|
22
|
+
* for a busy folder, not a real-world binding constraint (unlike `FolderSyncCommand`'s much smaller folder
|
|
23
|
+
* hierarchy, an Inbox can easily exceed this in one round). */
|
|
24
|
+
const DEFAULT_WINDOW_SIZE = 100;
|
|
25
|
+
/**
|
|
26
|
+
* Handles EAS `Sync`: enumerates `Add`/`Change`/`Delete`s for a single folder's contents since the device's last
|
|
27
|
+
* `Sync` of that folder, using the same watermark-based cursor mechanism `FolderSyncCommand` uses (via
|
|
28
|
+
* `EasSyncKeyUtils`), scoped by `folderUid` instead of `mailboxUid`, and keyed per-folder in
|
|
29
|
+
* `DeviceSyncState.folderSyncKeys` (the `CollectionId` a client sends *is* the `folderUid` - this library never
|
|
30
|
+
* invents a separate collection identifier).
|
|
31
|
+
*
|
|
32
|
+
* **Multi-collection requests**: every `<Collection>` in a request's `<Collections>` is processed and gets its
|
|
33
|
+
* own `<Collection>` entry in the response, each with its own independent `SyncKey`/`Status` - a client
|
|
34
|
+
* syncing several folders in one round trip (the common case once the initial per-folder backlog is done)
|
|
35
|
+
* gets one response covering all of them. All per-collection `SyncKey`/remembered-`Class` writes for the whole
|
|
36
|
+
* request are batched into a single `persistDeviceSyncState` call after every collection has been processed
|
|
37
|
+
* (never one call per collection) - see `EasSyncKeyUtils.persistDeviceSyncState`'s own doc comment for why a
|
|
38
|
+
* second write to the same `DeviceSyncState` within one request must never be built off a stale copy.
|
|
39
|
+
*
|
|
40
|
+
* **`Class` is only required on a collection's first (`SyncKey "0"`) request**, per `[MS-ASCMD]` - once seen,
|
|
41
|
+
* it's remembered in `DeviceSyncState.folderCollectionClasses` (keyed by `folderUid`) so a later request may
|
|
42
|
+
* omit it; omitting it for a folder never previously synced still gets `Status 4` (nothing to fall back to).
|
|
43
|
+
*
|
|
44
|
+
* **Every `CollectionId` is ACL-checked against the caller before it's touched**: `processCollection()` requires
|
|
45
|
+
* `ACLAction.READ` on the folder before enumerating or accepting any Commands for it at all (a folder the caller
|
|
46
|
+
* can't read is reported the same as an unrecognized collection - Status `4` - rather than leaking whether it
|
|
47
|
+
* exists); `applyAdd`/`applyChange`/`applyDelete` additionally require `CREATE`/`UPDATE`/`DELETE` respectively,
|
|
48
|
+
* and `applyChange`/`applyDelete` re-verify the resolved item's own `folderUid` actually matches the collection
|
|
49
|
+
* being synced (treating a mismatch identically to "not found" - Status `8` - never revealing that the
|
|
50
|
+
* `ServerId` resolves to something real elsewhere). Without this, a client could supply any other mailbox's
|
|
51
|
+
* folder/item uid as its own `CollectionId`/`ServerId` and read or mutate that mailbox's data directly - the
|
|
52
|
+
* same ownership-verification-after-an-`ignoreACL`-lookup pattern `ItemOperationsCommand`/`MoveItemsCommand`
|
|
53
|
+
* already use, applied consistently here too.
|
|
54
|
+
*
|
|
55
|
+
* **Pragmatic subset, deliberately not the full MS-ASCMD `Sync` surface**:
|
|
56
|
+
* - **Client-originated `Add`/`Change`/`Delete` commands are accepted for every collection type**, including
|
|
57
|
+
* `Email` (a device creating/editing a Draft, or deleting a message locally - see `applyAdd`/`applyChange`/
|
|
58
|
+
* `applyDelete`). `[MS-ASCMD]` itself disallows `Add`/`Change` for any *non-draft* `Email` item - this library
|
|
59
|
+
* doesn't verify a Sync `Email` Add/Change actually targets the caller's own Drafts folder *specifically*
|
|
60
|
+
* (only that it's a folder the caller actually owns/can write to - see above), matching how Contacts/Calendar/
|
|
61
|
+
* Tasks folder targeting is equally unchecked beyond ownership elsewhere. A collection whose adapter has no
|
|
62
|
+
* `fromApplicationData` at all would get Status `6` for `Add`/`Change` instead, but every adapter today
|
|
63
|
+
* implements it. Per `[MS-ASCMD]`'s own "Add (Sync)"/"Status (Sync)" pages: `Add` always gets a `Responses`
|
|
64
|
+
* entry (it must report the assigned `ServerId`); `Change`/`Delete` only get one on **failure** - a silent
|
|
65
|
+
* success means "assume it worked."
|
|
66
|
+
* - Only a body preview is returned per item (see `EmailSyncAdapter`'s own doc comment) - full body content is
|
|
67
|
+
* fetched separately via `ItemOperationsCommand`. A Draft's `Email` Add/Change is plain-text-only, with no
|
|
68
|
+
* attachment support (mirrors `ComposeMailCommand`'s own already-documented attachment gap).
|
|
69
|
+
*
|
|
70
|
+
* @author Jean-Philippe Steinmetz
|
|
71
|
+
*/
|
|
72
|
+
export class SyncCommand {
|
|
73
|
+
constructor() {
|
|
74
|
+
this.command = "Sync";
|
|
75
|
+
this.windowSize = DEFAULT_WINDOW_SIZE;
|
|
76
|
+
this.repos = new Map();
|
|
77
|
+
this.adapters = new Map();
|
|
78
|
+
}
|
|
79
|
+
async init() {
|
|
80
|
+
this.mailboxRepo = await this._objectFactory.newInstance(RepoUtils, {
|
|
81
|
+
name: this.mailboxClass.name,
|
|
82
|
+
args: [this.mailboxClass],
|
|
83
|
+
});
|
|
84
|
+
for (const [collectionClass, binding] of Object.entries(this.collectionBindings)) {
|
|
85
|
+
// RecoverableRepoUtils, not plain RepoUtils: SyncCommand now originates its own deletes
|
|
86
|
+
// (`applyDelete`) - without it, a soft-delete here wouldn't bump `dateModified`/`version`,
|
|
87
|
+
// breaking this exact class's own watermark-based deletion detection for anything deleted via
|
|
88
|
+
// Sync instead of the REST API. The same fix `BaseMapiEmsmdbRoute.ts` already needed for MAPI.
|
|
89
|
+
this.repos.set(collectionClass, await this._objectFactory.newInstance(RecoverableRepoUtils, {
|
|
90
|
+
name: binding.entityClass.name,
|
|
91
|
+
args: [binding.entityClass],
|
|
92
|
+
}));
|
|
93
|
+
this.adapters.set(collectionClass, await this._objectFactory.newInstance(binding.adapterClass));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Resolves the caller's own `Mailbox` at most once per request, and only if actually needed - most `Sync`
|
|
97
|
+
* requests contain no client-originated `Add` at all, so most requests never pay this extra round trip. */
|
|
98
|
+
mailboxLoader(ctx) {
|
|
99
|
+
let cached;
|
|
100
|
+
return async () => {
|
|
101
|
+
if (!cached) {
|
|
102
|
+
cached = await this.mailboxRepo.findOne(ctx.mailboxUid, { ignoreACL: true });
|
|
103
|
+
if (!cached) {
|
|
104
|
+
throw new ApiError(ApiErrors.NOT_FOUND, 404, "The caller's own mailbox no longer exists.");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return cached;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async handle(ctx) {
|
|
111
|
+
if (!this.aclUtils) {
|
|
112
|
+
throw new ApiError(ApiErrors.INTERNAL_ERROR, 500, ApiErrorMessages.INTERNAL_ERROR);
|
|
113
|
+
}
|
|
114
|
+
const collections = ctx.request ? findChild(ctx.request, "Collections") : undefined;
|
|
115
|
+
const collectionEls = collections ? findChildren(collections, "Collection") : [];
|
|
116
|
+
if (collectionEls.length === 0) {
|
|
117
|
+
return element(WbxmlCodePage.AirSync, "Sync", [textElement(WbxmlCodePage.AirSync, "Status", "3")]);
|
|
118
|
+
}
|
|
119
|
+
const collectionElements = [];
|
|
120
|
+
// Accumulated across every collection below, then written in exactly ONE persistDeviceSyncState call
|
|
121
|
+
// after the loop - never one call per collection (see this class's own doc comment on why).
|
|
122
|
+
let folderSyncKeys;
|
|
123
|
+
let folderCollectionClasses;
|
|
124
|
+
const getMailbox = this.mailboxLoader(ctx);
|
|
125
|
+
for (const collectionEl of collectionEls) {
|
|
126
|
+
const result = await this.processCollection(ctx, collectionEl, getMailbox);
|
|
127
|
+
collectionElements.push(result.collectionElement);
|
|
128
|
+
if (result.folderUid && result.newSyncKey) {
|
|
129
|
+
folderSyncKeys = {
|
|
130
|
+
...(folderSyncKeys ?? ctx.deviceSyncState.folderSyncKeys),
|
|
131
|
+
[result.folderUid]: result.newSyncKey,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (result.folderUid && result.rememberedClass) {
|
|
135
|
+
folderCollectionClasses = {
|
|
136
|
+
...(folderCollectionClasses ?? ctx.deviceSyncState.folderCollectionClasses ?? {}),
|
|
137
|
+
[result.folderUid]: result.rememberedClass,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (folderSyncKeys || folderCollectionClasses) {
|
|
142
|
+
await persistDeviceSyncState(ctx.deviceSyncState, ctx.deviceSyncStateRepo, {
|
|
143
|
+
...(folderSyncKeys ? { folderSyncKeys } : {}),
|
|
144
|
+
...(folderCollectionClasses ? { folderCollectionClasses } : {}),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return element(WbxmlCodePage.AirSync, "Sync", [
|
|
148
|
+
element(WbxmlCodePage.AirSync, "Collections", collectionElements),
|
|
149
|
+
]);
|
|
150
|
+
}
|
|
151
|
+
/** Processes one `<Collection>` from the request into its own `<Collection>` response element, plus (when
|
|
152
|
+
* this round advanced anything) the `folderUid`/new `SyncKey`/remembered `Class` for `handle()` to fold
|
|
153
|
+
* into its single end-of-request `persistDeviceSyncState` call - this method itself never persists
|
|
154
|
+
* anything, so it's safe to call once per collection in a request without the write-batching hazard
|
|
155
|
+
* `EasSyncKeyUtils.persistDeviceSyncState`'s doc comment describes. */
|
|
156
|
+
async processCollection(ctx, collectionEl, getMailbox) {
|
|
157
|
+
const requestedClass = childText(collectionEl, "Class");
|
|
158
|
+
const folderUid = childText(collectionEl, "CollectionId");
|
|
159
|
+
const clientSyncKey = childText(collectionEl, "SyncKey");
|
|
160
|
+
// Class is only required on a collection's first (SyncKey "0") request - see this class's own doc
|
|
161
|
+
// comment. A folder never previously synced has no remembered value to fall back to, so omitting Class
|
|
162
|
+
// there still (correctly) falls through to the "missing" branch below.
|
|
163
|
+
const collectionClass = requestedClass ?? (folderUid ? ctx.deviceSyncState.folderCollectionClasses?.[folderUid] : undefined);
|
|
164
|
+
if (!collectionClass || !folderUid) {
|
|
165
|
+
return { collectionElement: this.collectionResponse(collectionClass, folderUid, "4", clientSyncKey) };
|
|
166
|
+
}
|
|
167
|
+
const repo = this.repos.get(collectionClass);
|
|
168
|
+
const adapter = this.adapters.get(collectionClass);
|
|
169
|
+
if (!repo || !adapter) {
|
|
170
|
+
return { collectionElement: this.collectionResponse(collectionClass, folderUid, "4", clientSyncKey) };
|
|
171
|
+
}
|
|
172
|
+
// A folder the caller can't even read is reported identically to an unrecognized collection - never
|
|
173
|
+
// reveal whether a client-supplied CollectionId belonging to someone else's mailbox actually exists.
|
|
174
|
+
// See this class's own doc comment for why this check (and the matching ones in applyAdd/applyChange/
|
|
175
|
+
// applyDelete below) is required, not optional.
|
|
176
|
+
if (!(await this.aclUtils.hasPermission(ctx.user, folderUid, ACLAction.READ))) {
|
|
177
|
+
return { collectionElement: this.collectionResponse(collectionClass, folderUid, "4", clientSyncKey) };
|
|
178
|
+
}
|
|
179
|
+
const storedSyncKey = ctx.deviceSyncState.folderSyncKeys[folderUid];
|
|
180
|
+
const resolution = resolveSyncKey(clientSyncKey, storedSyncKey);
|
|
181
|
+
if (resolution.kind === "invalid") {
|
|
182
|
+
return { collectionElement: this.collectionResponse(collectionClass, folderUid, "3", undefined) };
|
|
183
|
+
}
|
|
184
|
+
if (resolution.kind === "initial") {
|
|
185
|
+
// Same epoch-not-"now" reasoning as FolderSyncCommand's own initial-sync branch: the client's next
|
|
186
|
+
// request (echoing this key) is its true first full sync of this folder and must see every
|
|
187
|
+
// existing item as an Add, not just ones modified after this handshake started.
|
|
188
|
+
const newKey = formatSyncKey({ generation: 1, watermark: new Date(0) });
|
|
189
|
+
return {
|
|
190
|
+
collectionElement: this.collectionResponse(collectionClass, folderUid, "1", newKey),
|
|
191
|
+
folderUid,
|
|
192
|
+
newSyncKey: newKey,
|
|
193
|
+
rememberedClass: requestedClass,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// Computed against the OLD watermark, BEFORE this round's own client-originated writes below are
|
|
197
|
+
// applied - this is what stops a client's own fresh Add/Change/Delete from being echoed straight back
|
|
198
|
+
// as a Commands/Add|Change|Delete in this SAME response.
|
|
199
|
+
const changes = await computeChanges(repo, "folderUid", folderUid, resolution.key.watermark, this.windowSize);
|
|
200
|
+
// Process the client's own Commands (if any) - after the read above, before computing the new SyncKey
|
|
201
|
+
// below (which must cover these writes too, or the NEXT round would re-report them as incoming
|
|
202
|
+
// server-side changes).
|
|
203
|
+
const requestCommands = findChild(collectionEl, "Commands");
|
|
204
|
+
const responseEntries = [];
|
|
205
|
+
let maxWriteWatermark;
|
|
206
|
+
const note = (date) => {
|
|
207
|
+
if (date && (!maxWriteWatermark || date > maxWriteWatermark))
|
|
208
|
+
maxWriteWatermark = date;
|
|
209
|
+
};
|
|
210
|
+
if (requestCommands) {
|
|
211
|
+
for (const el of findChildren(requestCommands, "Add")) {
|
|
212
|
+
const outcome = await this.applyAdd(ctx, adapter, repo, folderUid, el, getMailbox);
|
|
213
|
+
if (outcome.response)
|
|
214
|
+
responseEntries.push(outcome.response);
|
|
215
|
+
note(outcome.writtenAt);
|
|
216
|
+
}
|
|
217
|
+
for (const el of findChildren(requestCommands, "Change")) {
|
|
218
|
+
const outcome = await this.applyChange(ctx, adapter, repo, folderUid, el);
|
|
219
|
+
if (outcome.response)
|
|
220
|
+
responseEntries.push(outcome.response);
|
|
221
|
+
note(outcome.writtenAt);
|
|
222
|
+
}
|
|
223
|
+
for (const el of findChildren(requestCommands, "Delete")) {
|
|
224
|
+
const outcome = await this.applyDelete(ctx, repo, folderUid, el);
|
|
225
|
+
if (outcome.response)
|
|
226
|
+
responseEntries.push(outcome.response);
|
|
227
|
+
note(outcome.writtenAt);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Only ever extends the watermark forward past what `computeChanges` itself already determined - never
|
|
231
|
+
// jumps all the way to "now" unconditionally, which would silently skip over not-yet-enumerated
|
|
232
|
+
// pending changes whenever `changes.moreAvailable` is true.
|
|
233
|
+
const newWatermark = maxWriteWatermark && maxWriteWatermark > changes.newWatermark ? maxWriteWatermark : changes.newWatermark;
|
|
234
|
+
const newKey = formatSyncKey({ generation: resolution.key.generation + 1, watermark: newWatermark });
|
|
235
|
+
const totalChanges = changes.adds.length + changes.changes.length + changes.deletes.length;
|
|
236
|
+
if (totalChanges === 0 && responseEntries.length === 0) {
|
|
237
|
+
return {
|
|
238
|
+
collectionElement: this.collectionResponse(collectionClass, folderUid, "1", newKey),
|
|
239
|
+
folderUid,
|
|
240
|
+
newSyncKey: newKey,
|
|
241
|
+
rememberedClass: requestedClass,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const commandElements = [
|
|
245
|
+
...changes.adds.map((item) => this.itemToCommandElement("Add", adapter, item)),
|
|
246
|
+
...changes.changes.map((item) => this.itemToCommandElement("Change", adapter, item)),
|
|
247
|
+
...changes.deletes.map((item) => element(WbxmlCodePage.AirSync, "Delete", [textElement(WbxmlCodePage.AirSync, "ServerId", item.uid)])),
|
|
248
|
+
];
|
|
249
|
+
return {
|
|
250
|
+
collectionElement: this.collectionResponse(collectionClass, folderUid, "1", newKey, [
|
|
251
|
+
...(changes.moreAvailable ? [element(WbxmlCodePage.AirSync, "MoreAvailable", [])] : []),
|
|
252
|
+
...(commandElements.length > 0 ? [element(WbxmlCodePage.AirSync, "Commands", commandElements)] : []),
|
|
253
|
+
...(responseEntries.length > 0 ? [element(WbxmlCodePage.AirSync, "Responses", responseEntries)] : []),
|
|
254
|
+
]),
|
|
255
|
+
folderUid,
|
|
256
|
+
newSyncKey: newKey,
|
|
257
|
+
rememberedClass: requestedClass,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
itemToCommandElement(kind, adapter, item) {
|
|
261
|
+
return element(WbxmlCodePage.AirSync, kind, [
|
|
262
|
+
textElement(WbxmlCodePage.AirSync, "ServerId", item.uid),
|
|
263
|
+
adapter.toApplicationData(item),
|
|
264
|
+
]);
|
|
265
|
+
}
|
|
266
|
+
/** One client-originated command's outcome: `response` is a `Responses/{Add,Change,Delete}` entry to
|
|
267
|
+
* include (per MS-ASCMD, always present for `Add`, only present on failure for `Change`/`Delete`);
|
|
268
|
+
* `writtenAt` is the resulting `dateModified` of whatever was actually written, used to advance the
|
|
269
|
+
* persisted watermark past this round's own writes (see `handle()`'s own comment on why). */
|
|
270
|
+
addResponseElement(clientId, serverId, status) {
|
|
271
|
+
return element(WbxmlCodePage.AirSync, "Add", [
|
|
272
|
+
...(clientId ? [textElement(WbxmlCodePage.AirSync, "ClientId", clientId)] : []),
|
|
273
|
+
...(serverId ? [textElement(WbxmlCodePage.AirSync, "ServerId", serverId)] : []),
|
|
274
|
+
textElement(WbxmlCodePage.AirSync, "Status", status),
|
|
275
|
+
]);
|
|
276
|
+
}
|
|
277
|
+
statusResponseElement(kind, serverId, status) {
|
|
278
|
+
return element(WbxmlCodePage.AirSync, kind, [
|
|
279
|
+
textElement(WbxmlCodePage.AirSync, "ServerId", serverId),
|
|
280
|
+
textElement(WbxmlCodePage.AirSync, "Status", status),
|
|
281
|
+
]);
|
|
282
|
+
}
|
|
283
|
+
async applyAdd(ctx, adapter, repo, folderUid, el, getMailbox) {
|
|
284
|
+
const clientId = childText(el, "ClientId");
|
|
285
|
+
const appData = findChild(el, "ApplicationData");
|
|
286
|
+
// [MS-ASCMD] "Add (Sync)": "The Add element cannot be used to add any non-draft email items from the
|
|
287
|
+
// client to the server" - this pragmatic subset extends that same Status 6 to every collection whose
|
|
288
|
+
// adapter has no fromApplicationData at all, rather than special-casing "Email" by name.
|
|
289
|
+
if (!adapter.fromApplicationData || !appData) {
|
|
290
|
+
return { response: this.addResponseElement(clientId, undefined, "6") };
|
|
291
|
+
}
|
|
292
|
+
// The top-level per-collection READ check (processCollection) only proves the caller can see this
|
|
293
|
+
// folder - a shared/read-only folder still needs its own CREATE check before anything is written into it.
|
|
294
|
+
if (!(await this.aclUtils.hasPermission(ctx.user, folderUid, ACLAction.CREATE))) {
|
|
295
|
+
return { response: this.addResponseElement(clientId, undefined, "6") };
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
const defaults = adapter.newEntityDefaults ? adapter.newEntityDefaults(await getMailbox()) : {};
|
|
299
|
+
const partial = await adapter.fromApplicationData(appData);
|
|
300
|
+
const created = await repo.create({ ...defaults, ...partial, mailboxUid: ctx.mailboxUid, folderUid }, { ignoreACL: true });
|
|
301
|
+
return {
|
|
302
|
+
response: this.addResponseElement(clientId, created.uid, "1"),
|
|
303
|
+
writtenAt: created.dateModified,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// A malformed/invalid item (bad enum value, missing required field like Calendar's OrganizerEmail,
|
|
308
|
+
// ...) - Status 6 is [MS-ASCMD]'s own designated code for exactly this ("client/server conversion
|
|
309
|
+
// error... client has sent a malformed or invalid item").
|
|
310
|
+
return { response: this.addResponseElement(clientId, undefined, "6") };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async applyChange(ctx, adapter, repo, folderUid, el) {
|
|
314
|
+
const serverId = childText(el, "ServerId");
|
|
315
|
+
if (!serverId) {
|
|
316
|
+
return {};
|
|
317
|
+
}
|
|
318
|
+
if (!adapter.fromApplicationData) {
|
|
319
|
+
return { response: this.statusResponseElement("Change", serverId, "6") };
|
|
320
|
+
}
|
|
321
|
+
const existing = await repo.findOne(serverId, { ignoreACL: true });
|
|
322
|
+
// A ServerId that resolves to an item outside this (already ACL-verified-for-READ) collection is
|
|
323
|
+
// reported identically to "doesn't exist" - never reveal that it's real, just filed elsewhere (a
|
|
324
|
+
// different folder, or another mailbox's entirely). See this class's own doc comment.
|
|
325
|
+
if (!existing || existing.folderUid !== folderUid) {
|
|
326
|
+
return { response: this.statusResponseElement("Change", serverId, "8") };
|
|
327
|
+
}
|
|
328
|
+
if (!(await this.aclUtils.hasPermission(ctx.user, folderUid, ACLAction.UPDATE))) {
|
|
329
|
+
return { response: this.statusResponseElement("Change", serverId, "6") };
|
|
330
|
+
}
|
|
331
|
+
const appData = findChild(el, "ApplicationData");
|
|
332
|
+
if (!appData) {
|
|
333
|
+
return { response: this.statusResponseElement("Change", serverId, "6") };
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
const partial = await adapter.fromApplicationData(appData, existing);
|
|
337
|
+
const updated = await repo.update({ uid: existing.uid, version: existing.version, ...partial }, existing, { ignoreACL: true });
|
|
338
|
+
// Success is silent per [MS-ASCMD]'s own "the client only receives responses for ... failed
|
|
339
|
+
// changes" rule - no response entry.
|
|
340
|
+
return { writtenAt: updated.dateModified };
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
if (err instanceof ApiError && err.code === ApiErrors.INVALID_OBJECT_VERSION) {
|
|
344
|
+
return { response: this.statusResponseElement("Change", serverId, "7") };
|
|
345
|
+
}
|
|
346
|
+
return { response: this.statusResponseElement("Change", serverId, "6") };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async applyDelete(ctx, repo, folderUid, el) {
|
|
350
|
+
const serverId = childText(el, "ServerId");
|
|
351
|
+
if (!serverId) {
|
|
352
|
+
return {};
|
|
353
|
+
}
|
|
354
|
+
const existing = await repo.findOne(serverId, { ignoreACL: true });
|
|
355
|
+
// Same "treat as not found" rule applyChange uses - see its own comment.
|
|
356
|
+
if (!existing || existing.folderUid !== folderUid) {
|
|
357
|
+
return { response: this.statusResponseElement("Delete", serverId, "8") };
|
|
358
|
+
}
|
|
359
|
+
if (!(await this.aclUtils.hasPermission(ctx.user, folderUid, ACLAction.DELETE))) {
|
|
360
|
+
return { response: this.statusResponseElement("Delete", serverId, "6") };
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
await repo.delete(existing.uid, { ignoreACL: true });
|
|
364
|
+
// Re-read the now-soft-deleted row's own `dateModified` (`RecoverableRepoUtils.delete()` stamps it
|
|
365
|
+
// as part of the delete itself) rather than approximating with a fresh `new Date()` here. A plain
|
|
366
|
+
// `new Date()` captured after the write resolves is always >= that real timestamp (the delete's own
|
|
367
|
+
// internal write already completed by the time this line runs) - close enough for THIS row, but
|
|
368
|
+
// `newWatermark` is folder-wide: if it's inflated even slightly past this row's true write time, it
|
|
369
|
+
// can also run past a genuinely concurrent, unrelated write to a DIFFERENT message in the same
|
|
370
|
+
// folder that `computeChanges` already missed (it snapshotted before this Delete ran), permanently
|
|
371
|
+
// skipping that other change instead of picking it up next round. Falls back to `new Date()` only
|
|
372
|
+
// if the re-read is unexpectedly empty, which real code paths never hit.
|
|
373
|
+
const deleted = await repo.findOne(existing.uid, { ignoreACL: true, includeDeleted: true });
|
|
374
|
+
return { writtenAt: deleted?.dateModified ?? new Date() };
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return { response: this.statusResponseElement("Delete", serverId, "6") };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/** Builds one `<Collection>` response element - `handle()` collects one of these per request `<Collection>`
|
|
381
|
+
* and wraps the whole set in a single `<Sync><Collections>`. */
|
|
382
|
+
collectionResponse(collectionClass, folderUid, status, syncKey, extra = []) {
|
|
383
|
+
return element(WbxmlCodePage.AirSync, "Collection", [
|
|
384
|
+
...(collectionClass ? [textElement(WbxmlCodePage.AirSync, "Class", collectionClass)] : []),
|
|
385
|
+
...(syncKey ? [textElement(WbxmlCodePage.AirSync, "SyncKey", syncKey)] : []),
|
|
386
|
+
...(folderUid ? [textElement(WbxmlCodePage.AirSync, "CollectionId", folderUid)] : []),
|
|
387
|
+
textElement(WbxmlCodePage.AirSync, "Status", status),
|
|
388
|
+
...extra,
|
|
389
|
+
]);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
__decorate([
|
|
393
|
+
Config("mail:eas:sync_window_size", DEFAULT_WINDOW_SIZE),
|
|
394
|
+
__metadata("design:type", Number)
|
|
395
|
+
], SyncCommand.prototype, "windowSize", void 0);
|
|
396
|
+
__decorate([
|
|
397
|
+
Inject(ACLUtils),
|
|
398
|
+
__metadata("design:type", ACLUtils)
|
|
399
|
+
], SyncCommand.prototype, "aclUtils", void 0);
|
|
400
|
+
__decorate([
|
|
401
|
+
Init,
|
|
402
|
+
__metadata("design:type", Function),
|
|
403
|
+
__metadata("design:paramtypes", []),
|
|
404
|
+
__metadata("design:returntype", Promise)
|
|
405
|
+
], SyncCommand.prototype, "init", null);
|
|
406
|
+
//# sourceMappingURL=SyncCommand.js.map
|