@rapidmx/activesync 1.0.0-beta.0

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