@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,89 @@
|
|
|
1
|
+
import type { RecoverableBaseEntity, RepoUtils } from "@rapidrest/service-core";
|
|
2
|
+
import type { DeviceSyncState } from "@rapidmx/restapi";
|
|
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,52 @@
|
|
|
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`. */
|
|
22
|
+
toApplicationData(item: T): WbxmlElement;
|
|
23
|
+
/**
|
|
24
|
+
* Parses one client-originated `Add`/`Change` command's `<ApplicationData>` element (`el`) into a partial
|
|
25
|
+
* entity update. Only fields actually present in `el` are included in the result - an omitted field means
|
|
26
|
+
* "unchanged" (MS-ASCMD's own "ghosted property" rule - see `SyncCommand`'s doc comment), never "clear this
|
|
27
|
+
* field" - which is what lets the same method serve both `Add` (the partial is merged onto a fresh
|
|
28
|
+
* `{mailboxUid, folderUid}` baseline) and `Change` (the partial is merged onto `existing`).
|
|
29
|
+
*
|
|
30
|
+
* May return a `Promise` - `EmailSyncAdapter` needs this for a Draft `Add`/`Change`'s body, which must be
|
|
31
|
+
* written to `BlobStore` before the resulting `bodyBlobKey` is known; every other adapter today returns a
|
|
32
|
+
* plain object, which `await`s through unchanged.
|
|
33
|
+
*
|
|
34
|
+
* @param el The command's `<ApplicationData>` element.
|
|
35
|
+
* @param existing The item being changed, for a `Change` command; `undefined` for `Add`. Adapters that
|
|
36
|
+
* need to know the item's current field values to correctly interpret a partial update - `EmailSyncAdapter`
|
|
37
|
+
* uses this to reuse an existing Draft's `bodyBlobKey` on `Change` rather than minting a new one.
|
|
38
|
+
*/
|
|
39
|
+
fromApplicationData?(el: WbxmlElement, existing?: T): Partial<T> | Promise<Partial<T>>;
|
|
40
|
+
/**
|
|
41
|
+
* Supplies default field values for a brand-new entity created via a client-originated `Add`, applied
|
|
42
|
+
* *before* `fromApplicationData`'s own partial is merged on top (so anything the client actually sent
|
|
43
|
+
* always wins). For defaults a fresh entity needs regardless of what the client sent - `CalendarSyncAdapter`
|
|
44
|
+
* uses this for `icalUid`/`sequence`, since EAS's own `Add` command has no wire representation for either
|
|
45
|
+
* (a device doesn't know or send an iCalendar UID) but a stored default of `""` for every Sync-created
|
|
46
|
+
* event (this model's own fallback, see `CalendarEventMongo`'s constructor) would violate RFC 5545's own
|
|
47
|
+
* uniqueness expectation for `UID`. `EmailSyncAdapter` uses `mailbox` to populate a new Draft's `from`.
|
|
48
|
+
* Optional; only implemented where a collection actually needs it - most adapters have no such gap (and
|
|
49
|
+
* ignore the `mailbox` parameter entirely).
|
|
50
|
+
*/
|
|
51
|
+
newEntityDefaults?(mailbox: Mailbox): Partial<T>;
|
|
52
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
* @author Jean-Philippe Steinmetz
|
|
25
|
+
*/
|
|
26
|
+
export declare class EmailSyncAdapter implements EasCollectionSyncAdapter<Message> {
|
|
27
|
+
readonly collectionClass = "Email";
|
|
28
|
+
private blobStore?;
|
|
29
|
+
toApplicationData(message: Message): WbxmlElement;
|
|
30
|
+
/**
|
|
31
|
+
* `Message.bodyBlobKey` is documented (see the `Message` interface itself) as holding raw MIME "unmodified
|
|
32
|
+
* from ingestion/send" - `ItemOperationsCommand.fetchMessage` parses it with `simpleParser` unconditionally
|
|
33
|
+
* for every message, Draft or not. A Draft's plain-text body is therefore wrapped in a minimal valid
|
|
34
|
+
* RFC 5322 message here (via `buildPlainTextMime`) rather than stored as bare text, so that contract holds
|
|
35
|
+
* for every consumer, not just this write path - a Draft created/edited via `Sync` must `Fetch` correctly
|
|
36
|
+
* the same way any other message does.
|
|
37
|
+
*/
|
|
38
|
+
fromApplicationData(el: WbxmlElement, existing?: Message): Promise<Partial<Message>>;
|
|
39
|
+
/** Defaults for a brand-new Draft created via a client-originated `Add` - `from` is the caller's own
|
|
40
|
+
* mailbox address, per `ComposeMailCommand`'s identical `{ address, type: RecipientType.TO }` shape
|
|
41
|
+
* convention for a `from` field (the `Recipient` struct's `type` is only meaningful for real recipients;
|
|
42
|
+
* it's reused here as a harmless placeholder). */
|
|
43
|
+
newEntityDefaults(mailbox: Mailbox): Partial<Message>;
|
|
44
|
+
}
|
|
45
|
+
/** Encodes `Message.conversationId` (an internal string uid) into the opaque binary blob MS-ASEMAIL2's
|
|
46
|
+
* `Email2:ConversationId` carries on the wire. The spec never mandates any particular binary format for this
|
|
47
|
+
* value - a real Exchange server mints a GUID, but a client only ever compares/echoes it byte-for-byte, never
|
|
48
|
+
* interprets it - so encoding the uid's own UTF-8 bytes directly (rather than hashing into a 16-byte GUID
|
|
49
|
+
* shape) is a valid, simpler choice that `decodeConversationId` can invert exactly, which `ItemOperationsCommand`
|
|
50
|
+
* relies on to resolve an `ItemOperations` `Move`'s `ConversationId` back into this same uid. */
|
|
51
|
+
export declare function encodeConversationId(conversationId: string): Buffer;
|
|
52
|
+
/** Reverse of `encodeConversationId`. */
|
|
53
|
+
export declare function decodeConversationId(opaque: Buffer): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type WbxmlElement } from "../codec/WbxmlElement.js";
|
|
2
|
+
import type { EasCollectionSyncAdapter } from "./EasCollectionSyncAdapter.js";
|
|
3
|
+
import { type Task } from "@rapidmx/restapi";
|
|
4
|
+
/**
|
|
5
|
+
* Maps `Task` to/from the EAS `Sync` `Tasks` collection class (MS-ASTASK).
|
|
6
|
+
*
|
|
7
|
+
* **Pragmatic subset**: only the `Utc*` (UTC Compact DateTime) variant of each date field is emitted, not the
|
|
8
|
+
* paired local-time variant MS-ASTASK also defines (`DueDate`/`StartDate` alongside `UtcDueDate`/
|
|
9
|
+
* `UtcStartDate`) - this library doesn't track a per-task timezone to correctly localize the non-UTC variant,
|
|
10
|
+
* and a real device already treats the `Utc*` field as authoritative. Recurring tasks are not synced (a task's
|
|
11
|
+
* `Recurrence` element mirrors Calendar's, itself already a pragmatic subset there - deferred further here).
|
|
12
|
+
*
|
|
13
|
+
* @author Jean-Philippe Steinmetz
|
|
14
|
+
*/
|
|
15
|
+
export declare class TasksSyncAdapter implements EasCollectionSyncAdapter<Task> {
|
|
16
|
+
readonly collectionClass = "Tasks";
|
|
17
|
+
toApplicationData(task: Task): WbxmlElement;
|
|
18
|
+
/**
|
|
19
|
+
* Reverse of `toApplicationData`. `DateCompleted` is never parsed back - it's `task.dateModified` echoed
|
|
20
|
+
* out, not an independent field this library's own `Task` model has room to store separately, so a client
|
|
21
|
+
* setting `Complete` is enough on its own. `ReminderSet="0"` (with no `ReminderTime`) is treated as an
|
|
22
|
+
* explicit "clear the reminder" signal (`reminderDate: null` in the returned partial, which - unlike
|
|
23
|
+
* simply omitting the key - does override an existing reminder when merged onto `existing` for a `Change`)
|
|
24
|
+
* since MS-ASTASK gives no other way to express removing a reminder; `UtcDueDate` has no equivalent
|
|
25
|
+
* explicit-clear signal and so can only be set, never cleared, via `Sync` - a real, narrower gap than
|
|
26
|
+
* `reminderDate`'s, documented here rather than silently accepted. `null`, not `undefined`: TypeORM's
|
|
27
|
+
* `UpdateQueryBuilder` silently drops any `undefined`-valued key from the generated SQL `SET` clause
|
|
28
|
+
* (confirmed by reading its source), so `undefined` here would leave a stale `reminderDate` in place on
|
|
29
|
+
* the SQL backend while correctly clearing it on Mongo - `null` clears it on both.
|
|
30
|
+
*/
|
|
31
|
+
fromApplicationData(el: WbxmlElement): Partial<Task>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WBXML code page index -> namespace name, per MS-ASWBXML §2.1.2.1 ("List of Code Pages"). A code page is
|
|
3
|
+
* WBXML's mechanism for reusing the single-byte tag-token space across many XML namespaces: a `SWITCH_PAGE`
|
|
4
|
+
* global token changes which of these tables subsequent tag bytes are looked up against.
|
|
5
|
+
*
|
|
6
|
+
* @author Jean-Philippe Steinmetz
|
|
7
|
+
*/
|
|
8
|
+
export declare enum WbxmlCodePage {
|
|
9
|
+
AirSync = 0,
|
|
10
|
+
Contacts = 1,
|
|
11
|
+
Email = 2,
|
|
12
|
+
AirNotification = 3,
|
|
13
|
+
Calendar = 4,
|
|
14
|
+
Move = 5,
|
|
15
|
+
ItemEstimate = 6,
|
|
16
|
+
FolderHierarchy = 7,
|
|
17
|
+
MeetingResponse = 8,
|
|
18
|
+
Tasks = 9,
|
|
19
|
+
ResolveRecipients = 10,
|
|
20
|
+
ValidateCert = 11,
|
|
21
|
+
Contacts2 = 12,
|
|
22
|
+
Ping = 13,
|
|
23
|
+
Provision = 14,
|
|
24
|
+
Search = 15,
|
|
25
|
+
Gal = 16,
|
|
26
|
+
AirSyncBase = 17,
|
|
27
|
+
Settings = 18,
|
|
28
|
+
DocumentLibrary = 19,
|
|
29
|
+
ItemOperations = 20,
|
|
30
|
+
ComposeMail = 21,
|
|
31
|
+
Email2 = 22,
|
|
32
|
+
Notes = 23,
|
|
33
|
+
RightsManagement = 24,
|
|
34
|
+
Find = 25
|
|
35
|
+
}
|
|
36
|
+
/** Resolves a decoded tag byte's low 6 bits back into its tag name for the given code page. Falls back to a
|
|
37
|
+
* synthetic `"Unknown0x.."` name (rather than throwing) for a page/code this table doesn't recognize - a
|
|
38
|
+
* still-decodable, debuggable placeholder for a deferred code page or a future protocol version's new field,
|
|
39
|
+
* rather than failing the entire document over one unrecognized tag. */
|
|
40
|
+
export declare function tagNameForCode(page: number, code: number): string;
|
|
41
|
+
/** Resolves a tag name to its token byte (low 6 bits, before the content-flag bit is applied) for the given
|
|
42
|
+
* code page. Unlike `tagNameForCode()`, this throws on an unknown pair - encoding a tag this table has no
|
|
43
|
+
* token for is always a genuine programming error (an unregistered/deferred field), never legitimate input to
|
|
44
|
+
* tolerate. */
|
|
45
|
+
export declare function codeForTagName(page: number, tag: string): number;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { WbxmlElement } from "./WbxmlElement.js";
|
|
2
|
+
/**
|
|
3
|
+
* Decodes a WBXML byte stream (an EAS request/response body) back into a `WbxmlElement` tree — the exact
|
|
4
|
+
* inverse of `WbxmlEncoder`. Reads the fixed EAS document header, skips its (always-empty, in real
|
|
5
|
+
* ActiveSync traffic) string table, then parses the single root element.
|
|
6
|
+
*
|
|
7
|
+
* The `publicid` header field is read via the same generic `mb_u_int32` reader used everywhere else, which is
|
|
8
|
+
* only a partial implementation of the full WBXML spec for that field (a raw leading `0x00` byte would
|
|
9
|
+
* signal "public identifier is a string-table reference" under the full spec, a form real ActiveSync traffic
|
|
10
|
+
* never uses) — safe here since every real EAS document sends the literal well-known value `1` ("unknown or
|
|
11
|
+
* missing"), which decodes identically either way.
|
|
12
|
+
*
|
|
13
|
+
* @author Jean-Philippe Steinmetz
|
|
14
|
+
*/
|
|
15
|
+
export declare class WbxmlDecoder {
|
|
16
|
+
private buf;
|
|
17
|
+
private pos;
|
|
18
|
+
private currentPage;
|
|
19
|
+
private depth;
|
|
20
|
+
decode(data: Buffer): WbxmlElement;
|
|
21
|
+
private readByte;
|
|
22
|
+
/** Decodes a WBXML `mb_u_int32`: base-128 digits, most significant group first, every byte but the last
|
|
23
|
+
* carrying the 0x80 continuation bit. Mirrors `WbxmlEncoder.writeMbUint()`. */
|
|
24
|
+
private readMbUint;
|
|
25
|
+
private readCString;
|
|
26
|
+
private readTagElement;
|
|
27
|
+
/** Reads a mixed sequence of child tag elements / an inline string / opaque binary content, up to (and
|
|
28
|
+
* consuming) the terminating `END` token — the body of one "has content" element. Also handles a
|
|
29
|
+
* `SWITCH_PAGE` appearing between sibling children, which applies to every subsequent sibling until
|
|
30
|
+
* either the next switch or the end of this content block (switching page is a standing instruction, not
|
|
31
|
+
* scoped to a single following tag). */
|
|
32
|
+
private readContentUntilEnd;
|
|
33
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal, tagged-tree representation of one WBXML element, decoupled from the wire format's binary token
|
|
3
|
+
* encoding — `WbxmlEncoder`/`WbxmlDecoder` are the only code that ever deals in raw bytes; every EAS command
|
|
4
|
+
* handler works against this tree.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately does not model WBXML attributes: MS-ASWBXML's own algorithm description confirms ActiveSync
|
|
7
|
+
* never uses the attribute mechanism (every element's data is carried as child elements or inline text), so
|
|
8
|
+
* there is nothing for an `attrs` field to represent here.
|
|
9
|
+
*
|
|
10
|
+
* A given element is expected to carry exactly one of `children` (nested elements - the common case for a
|
|
11
|
+
* structural container like `Sync`/`Collection`), `text` (a single inline string value - the common case for
|
|
12
|
+
* a leaf field like `SyncKey`/`ServerId`), or `opaque` (raw binary content - used only for `ItemOperations`
|
|
13
|
+
* `Data` and similar binary payloads). Real ActiveSync XML is data-oriented, not document-oriented, so this
|
|
14
|
+
* library never needs to represent mixed content (an element with both nested tags and inline text).
|
|
15
|
+
*
|
|
16
|
+
* @author Jean-Philippe Steinmetz
|
|
17
|
+
*/
|
|
18
|
+
export interface WbxmlElement {
|
|
19
|
+
/** The `WbxmlCodePage` this element's tag belongs to. */
|
|
20
|
+
page: number;
|
|
21
|
+
/** The tag name, as registered in `WbxmlCodePages.ts` for `page`. */
|
|
22
|
+
tag: string;
|
|
23
|
+
/** Nested elements, in document order. Empty for a leaf/empty element. */
|
|
24
|
+
children: WbxmlElement[];
|
|
25
|
+
/** Inline string content (WBXML `STR_I`), for a leaf element carrying a single text value. */
|
|
26
|
+
text?: string;
|
|
27
|
+
/** Raw binary content (WBXML `OPAQUE`), for a leaf element carrying binary data. */
|
|
28
|
+
opaque?: Buffer;
|
|
29
|
+
}
|
|
30
|
+
/** Constructs a structural element with nested children (e.g. `<Collection>...</Collection>`). */
|
|
31
|
+
export declare function element(page: number, tag: string, children?: WbxmlElement[]): WbxmlElement;
|
|
32
|
+
/** Constructs a leaf element carrying a single inline string value (e.g. `<SyncKey>1</SyncKey>`). */
|
|
33
|
+
export declare function textElement(page: number, tag: string, text: string): WbxmlElement;
|
|
34
|
+
/** Constructs a leaf element carrying raw binary content (e.g. `<Data>...</Data>` in `ItemOperations`). */
|
|
35
|
+
export declare function opaqueElement(page: number, tag: string, opaque: Buffer): WbxmlElement;
|
|
36
|
+
/** Finds the first direct child of `parent` with the given tag name, or `undefined` if none exists. Does not
|
|
37
|
+
* search recursively — WBXML/EAS field lookups are always relative to a specific known parent element. */
|
|
38
|
+
export declare function findChild(parent: WbxmlElement, tag: string): WbxmlElement | undefined;
|
|
39
|
+
/** Finds every direct child of `parent` with the given tag name (e.g. every `Collection` under
|
|
40
|
+
* `Collections`). */
|
|
41
|
+
export declare function findChildren(parent: WbxmlElement, tag: string): WbxmlElement[];
|
|
42
|
+
/** Shorthand for `findChild(parent, tag)?.text`, the common case of reading a leaf field's string value. */
|
|
43
|
+
export declare function childText(parent: WbxmlElement, tag: string): string | undefined;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { WbxmlElement } from "./WbxmlElement.js";
|
|
2
|
+
/**
|
|
3
|
+
* Encodes a `WbxmlElement` tree into a WBXML byte stream, per MS-ASWBXML's encoding algorithm. Emits the
|
|
4
|
+
* fixed EAS document header (`version=1.3, publicid=unknown, charset=UTF-8, empty string table`) followed by
|
|
5
|
+
* the token stream for the given root element, switching code pages only when the page actually changes from
|
|
6
|
+
* whatever was last active (starting from `AirSync`, page 0, the implicit default per spec).
|
|
7
|
+
*
|
|
8
|
+
* @author Jean-Philippe Steinmetz
|
|
9
|
+
*/
|
|
10
|
+
export declare class WbxmlEncoder {
|
|
11
|
+
private bytes;
|
|
12
|
+
private currentPage;
|
|
13
|
+
encode(root: WbxmlElement): Buffer;
|
|
14
|
+
private writeElement;
|
|
15
|
+
private writeStrI;
|
|
16
|
+
private writeOpaque;
|
|
17
|
+
/** Encodes `value` as a WBXML multi-byte unsigned integer (`mb_u_int32`): base-128 digits, most
|
|
18
|
+
* significant group first, every byte but the last carrying the 0x80 continuation bit. */
|
|
19
|
+
private writeMbUint;
|
|
20
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type Message, RecoverableRepoUtils } from "@rapidmx/restapi";
|
|
2
|
+
import { type WbxmlElement } from "../codec/WbxmlElement.js";
|
|
3
|
+
import type { EasCommandContext, EasCommandHandler } from "../EasCommandHandler.js";
|
|
4
|
+
/**
|
|
5
|
+
* Shared implementation for EAS `SendMail`, `SmartForward`, and `SmartReply` (MS-ASCMD `ComposeMail` namespace)
|
|
6
|
+
* — all three submit a client-composed raw MIME body directly (`<Mime>`, opaque WBXML content) rather than
|
|
7
|
+
* referencing a pre-existing draft `Message`, unlike the webmail REST API's `POST /messages/:id/send` (see
|
|
8
|
+
* `BaseMessageRoute.send()`, which this class's `scanAndRelay()` call shares its scan-then-relay core with via
|
|
9
|
+
* `MailSendUtils.ts`).
|
|
10
|
+
*
|
|
11
|
+
* **Pragmatic subset, deliberately not the full MS-ASCMD semantics**:
|
|
12
|
+
* - `SmartForward`/`SmartReply`'s `<Source>` (the message being forwarded/replied to) is used only to thread
|
|
13
|
+
* the outgoing message (`inReplyTo`/`references`) and to flip the original's `Answered`/`Forwarded` flag - the
|
|
14
|
+
* real spec has the *server* splice the original message's full content into the outgoing MIME so the client
|
|
15
|
+
* never has to download-then-reupload it; this pragmatic subset instead expects the client's own `<Mime>` to
|
|
16
|
+
* already be the complete outgoing message (which is what every mainstream client's own compose UI naturally
|
|
17
|
+
* produces once it has fetched the original for display), matching this library's "pragmatic subset, not full
|
|
18
|
+
* fidelity" precedent elsewhere (e.g. `FolderSyncCommand`'s SyncKey replay-protection gap).
|
|
19
|
+
* - `ReplaceMime`/`AccountId`/`InstanceId` are not read - single-account, non-recurring-meeting compose only.
|
|
20
|
+
* - Attachments present in the composed MIME are relayed correctly (`scanAndRelay()`'s `ScanPipeline` handles
|
|
21
|
+
* the full raw message) but are not additionally persisted as `Attachment` records on the saved Sent Items
|
|
22
|
+
* copy - `Message.hasAttachments` is still set correctly from the parsed MIME, just not each attachment's own
|
|
23
|
+
* row (deferred, matching `ItemOperationsCommand`'s own future `Fetch`-of-Sent-Items scope).
|
|
24
|
+
*
|
|
25
|
+
* `folderClass`/`messageClass` are supplied by the Mongo/SQL concrete subclasses, and `markOriginal()` by the
|
|
26
|
+
* `SmartForwardCommand`/`SmartReplyCommand` subclasses (a no-op here, since plain `SendMailCommand` never has a
|
|
27
|
+
* `<Source>` to act on).
|
|
28
|
+
*
|
|
29
|
+
* @author Jean-Philippe Steinmetz
|
|
30
|
+
*/
|
|
31
|
+
export declare abstract class ComposeMailCommand implements EasCommandHandler {
|
|
32
|
+
abstract readonly command: string;
|
|
33
|
+
protected abstract folderClass: any;
|
|
34
|
+
protected abstract messageClass: any;
|
|
35
|
+
private _objectFactory?;
|
|
36
|
+
protected folderRepo?: RecoverableRepoUtils<any>;
|
|
37
|
+
protected messageRepo?: RecoverableRepoUtils<any>;
|
|
38
|
+
private blobStore?;
|
|
39
|
+
private mailTransport?;
|
|
40
|
+
private scanPipeline?;
|
|
41
|
+
private aclUtils?;
|
|
42
|
+
init(): Promise<void>;
|
|
43
|
+
/** Called once the outgoing message has been sent, only when the request carried a `<Source>` (i.e. this
|
|
44
|
+
* is a `SmartForward`/`SmartReply`, never a plain `SendMail`) - flips the referenced original message's own
|
|
45
|
+
* `Answered`/`Forwarded` flag. A no-op here; overridden by the two subclasses that need it. */
|
|
46
|
+
protected markOriginal(_ctx: EasCommandContext, _original: Message & {
|
|
47
|
+
uid: string;
|
|
48
|
+
}): Promise<void>;
|
|
49
|
+
handle(ctx: EasCommandContext): Promise<WbxmlElement | undefined>;
|
|
50
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type WbxmlElement } from "../codec/WbxmlElement.js";
|
|
2
|
+
import type { EasCommandContext, EasCommandHandler } from "../EasCommandHandler.js";
|
|
3
|
+
import { Folder } from "@rapidmx/restapi";
|
|
4
|
+
/**
|
|
5
|
+
* Handles EAS `FolderSync`: enumerates `Add`/`Update`/`Delete`s for the caller's mailbox's `Folder` hierarchy
|
|
6
|
+
* since the device's last `FolderSync`, using the shared watermark-based cursor mechanism in
|
|
7
|
+
* `EasSyncKeyUtils.ts` (scoped by `mailboxUid` over the `Folder` collection, rather than `folderUid` over a
|
|
8
|
+
* per-folder item collection the way `SyncCommand` will be).
|
|
9
|
+
*
|
|
10
|
+
* `folderClass` is supplied by the Mongo/SQL concrete subclasses, following the exact one-line-per-backend
|
|
11
|
+
* pattern used throughout this library's other routes/jobs.
|
|
12
|
+
*
|
|
13
|
+
* @author Jean-Philippe Steinmetz
|
|
14
|
+
*/
|
|
15
|
+
export declare abstract class FolderSyncCommand<F extends Folder> implements EasCommandHandler {
|
|
16
|
+
readonly command = "FolderSync";
|
|
17
|
+
protected abstract folderClass: any;
|
|
18
|
+
private windowSize;
|
|
19
|
+
private _objectFactory?;
|
|
20
|
+
private folderRepo?;
|
|
21
|
+
init(): Promise<void>;
|
|
22
|
+
handle(ctx: EasCommandContext): Promise<WbxmlElement | undefined>;
|
|
23
|
+
private folderToChangeElement;
|
|
24
|
+
private persistSyncKey;
|
|
25
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type WbxmlElement } from "../codec/WbxmlElement.js";
|
|
2
|
+
import type { EasCommandContext, EasCommandHandler } from "../EasCommandHandler.js";
|
|
3
|
+
/** Binds one MS-ASCMD `Class` value to the concrete entity class this command counts against - a lighter
|
|
4
|
+
* version of `SyncCommand`'s own `SyncCollectionBinding` (no adapter needed at all, since `GetItemEstimate`
|
|
5
|
+
* never serializes an item, only counts them). Supplied by the Mongo/SQL concrete subclasses. */
|
|
6
|
+
export interface EstimateCollectionBinding {
|
|
7
|
+
entityClass: any;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Handles EAS `GetItemEstimate`: reports, per requested `<Collection>`, an estimated count of the
|
|
11
|
+
* `Add`/`Change`/`Delete`s a subsequent `Sync` of that collection would return - read-only, it never issues or
|
|
12
|
+
* consumes a `SyncKey` itself.
|
|
13
|
+
*
|
|
14
|
+
* The modern (14.0+) request/response reuses `WbxmlCodePage.AirSync`'s own `Collections`/`Collection`/`Class`/
|
|
15
|
+
* `CollectionId`/`SyncKey` via `SWITCH_PAGE` rather than this page's own legacy (`Folders`/`Folder`/`FolderId`)
|
|
16
|
+
* shape - see `WbxmlCodePages.ts`'s own doc comment on `WbxmlCodePage.ItemEstimate` for why, and
|
|
17
|
+
* `ItemOperationsCommand.fetchMessage`'s identical cross-page-reuse precedent.
|
|
18
|
+
*
|
|
19
|
+
* A `SyncKey` of `"0"` (or one this device has never synced this folder with before) reports the folder's
|
|
20
|
+
* total live item count - what a first `Sync` would report as `Add`s. Otherwise reuses `EasSyncKeyUtils.
|
|
21
|
+
* computeChanges()` (the same enumeration `SyncCommand` itself uses), capped at `DEFAULT_MAX_COUNT` - see its
|
|
22
|
+
* own doc comment for why this is a real, documented approximation on a very active folder rather than a
|
|
23
|
+
* precise unbounded count.
|
|
24
|
+
*
|
|
25
|
+
* **ACL-checked like `Sync`**: `estimateCollection()` requires `ACLAction.READ` on the client-supplied
|
|
26
|
+
* `CollectionId` before counting anything - without it, a crafted `CollectionId` belonging to another
|
|
27
|
+
* mailbox's folder would return a real pending-change count for it. A denied folder is reported identically to
|
|
28
|
+
* an unrecognized collection (`Status 2`), never distinguishable from "you don't have this collection at all".
|
|
29
|
+
*
|
|
30
|
+
* @author Jean-Philippe Steinmetz
|
|
31
|
+
*/
|
|
32
|
+
export declare abstract class GetItemEstimateCommand implements EasCommandHandler {
|
|
33
|
+
readonly command = "GetItemEstimate";
|
|
34
|
+
protected abstract collectionBindings: Record<string, EstimateCollectionBinding>;
|
|
35
|
+
private maxCount;
|
|
36
|
+
private _objectFactory?;
|
|
37
|
+
private aclUtils?;
|
|
38
|
+
private repos;
|
|
39
|
+
init(): Promise<void>;
|
|
40
|
+
handle(ctx: EasCommandContext): Promise<WbxmlElement | undefined>;
|
|
41
|
+
private estimateCollection;
|
|
42
|
+
private collectionElement;
|
|
43
|
+
}
|