@rapidmx/mapi-plugin 1.0.0-beta.3 → 1.0.0-beta.5

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 (154) hide show
  1. package/README.md +4 -4
  2. package/dist/lib/BaseMapiEmsmdbRoute.js +229 -37
  3. package/dist/lib/BaseMapiEmsmdbRoute.js.map +1 -1
  4. package/dist/lib/MapiSessionManager.js +404 -48
  5. package/dist/lib/MapiSessionManager.js.map +1 -1
  6. package/dist/lib/RegexPatternUtils.js +30 -0
  7. package/dist/lib/RegexPatternUtils.js.map +1 -0
  8. package/dist/lib/RopDispatcher.js +130 -19
  9. package/dist/lib/RopDispatcher.js.map +1 -1
  10. package/dist/lib/codec/AppointmentRecurrence.js +22 -21
  11. package/dist/lib/codec/AppointmentRecurrence.js.map +1 -1
  12. package/dist/lib/codec/BufferCursor.js +41 -12
  13. package/dist/lib/codec/BufferCursor.js.map +1 -1
  14. package/dist/lib/codec/GlobalObjectId.js +26 -20
  15. package/dist/lib/codec/GlobalObjectId.js.map +1 -1
  16. package/dist/lib/codec/MapiTimeZone.js +43 -10
  17. package/dist/lib/codec/MapiTimeZone.js.map +1 -1
  18. package/dist/lib/codec/PropertyValue.js +28 -2
  19. package/dist/lib/codec/PropertyValue.js.map +1 -1
  20. package/dist/lib/codec/RopBuffer.js +15 -3
  21. package/dist/lib/codec/RopBuffer.js.map +1 -1
  22. package/dist/lib/mongo/MapiEmsmdbRouteMongo.js +2 -1
  23. package/dist/lib/mongo/MapiEmsmdbRouteMongo.js.map +1 -1
  24. package/dist/lib/nspi/NspiCodec.js +5 -5
  25. package/dist/lib/nspi/NspiCodec.js.map +1 -1
  26. package/dist/lib/nspi/NspiGetMatchesHandler.js +14 -14
  27. package/dist/lib/nspi/NspiGetMatchesHandler.js.map +1 -1
  28. package/dist/lib/rop/AddressList.js +109 -0
  29. package/dist/lib/rop/AddressList.js.map +1 -0
  30. package/dist/lib/rop/CalendarEventTarget.js +7 -4
  31. package/dist/lib/rop/CalendarEventTarget.js.map +1 -1
  32. package/dist/lib/rop/ContactTarget.js +2 -6
  33. package/dist/lib/rop/ContactTarget.js.map +1 -1
  34. package/dist/lib/rop/ContentsTable.js +60 -0
  35. package/dist/lib/rop/ContentsTable.js.map +1 -0
  36. package/dist/lib/rop/ExecuteBudget.js +82 -0
  37. package/dist/lib/rop/ExecuteBudget.js.map +1 -0
  38. package/dist/lib/rop/FastTransferStream.js +99 -2
  39. package/dist/lib/rop/FastTransferStream.js.map +1 -1
  40. package/dist/lib/rop/FolderTarget.js +13 -6
  41. package/dist/lib/rop/FolderTarget.js.map +1 -1
  42. package/dist/lib/rop/HandleDataCache.js +412 -0
  43. package/dist/lib/rop/HandleDataCache.js.map +1 -0
  44. package/dist/lib/rop/MeetingMessageClassHandler.js +194 -48
  45. package/dist/lib/rop/MeetingMessageClassHandler.js.map +1 -1
  46. package/dist/lib/rop/MessageBodyStream.js +33 -7
  47. package/dist/lib/rop/MessageBodyStream.js.map +1 -1
  48. package/dist/lib/rop/MessageTarget.js +17 -4
  49. package/dist/lib/rop/MessageTarget.js.map +1 -1
  50. package/dist/lib/rop/NamedPropertyRegistry.js +8 -5
  51. package/dist/lib/rop/NamedPropertyRegistry.js.map +1 -1
  52. package/dist/lib/rop/PropertyResolvers.js +3 -1
  53. package/dist/lib/rop/PropertyResolvers.js.map +1 -1
  54. package/dist/lib/rop/RepoPaging.js +54 -0
  55. package/dist/lib/rop/RepoPaging.js.map +1 -0
  56. package/dist/lib/rop/RestapiRules.js +52 -0
  57. package/dist/lib/rop/RestapiRules.js.map +1 -0
  58. package/dist/lib/rop/RopCreateMessageHandler.js +4 -2
  59. package/dist/lib/rop/RopCreateMessageHandler.js.map +1 -1
  60. package/dist/lib/rop/RopDeleteFolderHandler.js +118 -77
  61. package/dist/lib/rop/RopDeleteFolderHandler.js.map +1 -1
  62. package/dist/lib/rop/RopDeleteMessagesHandler.js +26 -1
  63. package/dist/lib/rop/RopDeleteMessagesHandler.js.map +1 -1
  64. package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js +14 -14
  65. package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js.map +1 -1
  66. package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js +13 -14
  67. package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js.map +1 -1
  68. package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js +33 -32
  69. package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js.map +1 -1
  70. package/dist/lib/rop/RopGetContentsTableHandler.js +17 -38
  71. package/dist/lib/rop/RopGetContentsTableHandler.js.map +1 -1
  72. package/dist/lib/rop/RopGetHierarchyTableHandler.js +4 -2
  73. package/dist/lib/rop/RopGetHierarchyTableHandler.js.map +1 -1
  74. package/dist/lib/rop/RopGetPropertiesSpecificHandler.js +6 -8
  75. package/dist/lib/rop/RopGetPropertiesSpecificHandler.js.map +1 -1
  76. package/dist/lib/rop/RopHandler.js +5 -1
  77. package/dist/lib/rop/RopHandler.js.map +1 -1
  78. package/dist/lib/rop/RopLogonHandler.js +5 -2
  79. package/dist/lib/rop/RopLogonHandler.js.map +1 -1
  80. package/dist/lib/rop/RopOpenFolderHandler.js +3 -1
  81. package/dist/lib/rop/RopOpenFolderHandler.js.map +1 -1
  82. package/dist/lib/rop/RopOpenMessageHandler.js +3 -1
  83. package/dist/lib/rop/RopOpenMessageHandler.js.map +1 -1
  84. package/dist/lib/rop/RopOpenStreamHandler.js +11 -8
  85. package/dist/lib/rop/RopOpenStreamHandler.js.map +1 -1
  86. package/dist/lib/rop/RopQueryRowsHandler.js +31 -5
  87. package/dist/lib/rop/RopQueryRowsHandler.js.map +1 -1
  88. package/dist/lib/rop/RopReadStreamHandler.js +22 -4
  89. package/dist/lib/rop/RopReadStreamHandler.js.map +1 -1
  90. package/dist/lib/rop/RopReleaseHandler.js +4 -1
  91. package/dist/lib/rop/RopReleaseHandler.js.map +1 -1
  92. package/dist/lib/rop/RopSaveChangesMessageHandler.js +66 -7
  93. package/dist/lib/rop/RopSaveChangesMessageHandler.js.map +1 -1
  94. package/dist/lib/rop/RopSetColumnsHandler.js +6 -8
  95. package/dist/lib/rop/RopSetColumnsHandler.js.map +1 -1
  96. package/dist/lib/rop/RopSetPropertiesHandler.js +23 -12
  97. package/dist/lib/rop/RopSetPropertiesHandler.js.map +1 -1
  98. package/dist/lib/rop/RopSubmitMessageHandler.js +148 -120
  99. package/dist/lib/rop/RopSubmitMessageHandler.js.map +1 -1
  100. package/dist/lib/rop/RopWriteStreamHandler.js +37 -10
  101. package/dist/lib/rop/RopWriteStreamHandler.js.map +1 -1
  102. package/dist/lib/rop/TaskTarget.js +2 -6
  103. package/dist/lib/rop/TaskTarget.js.map +1 -1
  104. package/dist/lib/rop/TransportSend.js +16 -0
  105. package/dist/lib/rop/TransportSend.js.map +1 -0
  106. package/dist/lib/sql/MapiEmsmdbRouteSQL.js +2 -1
  107. package/dist/lib/sql/MapiEmsmdbRouteSQL.js.map +1 -1
  108. package/dist/types/BaseMapiEmsmdbRoute.d.ts +59 -2
  109. package/dist/types/MapiSessionManager.d.ts +218 -63
  110. package/dist/types/RegexPatternUtils.d.ts +14 -0
  111. package/dist/types/RopDispatcher.d.ts +47 -11
  112. package/dist/types/codec/BufferCursor.d.ts +14 -0
  113. package/dist/types/codec/GlobalObjectId.d.ts +9 -5
  114. package/dist/types/codec/PropertyValue.d.ts +10 -0
  115. package/dist/types/codec/RopBuffer.d.ts +7 -0
  116. package/dist/types/mongo/MapiEmsmdbRouteMongo.d.ts +1 -0
  117. package/dist/types/nspi/NspiGetMatchesHandler.d.ts +2 -0
  118. package/dist/types/rop/AddressList.d.ts +48 -0
  119. package/dist/types/rop/CalendarEventTarget.d.ts +7 -3
  120. package/dist/types/rop/ContactTarget.d.ts +3 -3
  121. package/dist/types/rop/ContentsTable.d.ts +16 -0
  122. package/dist/types/rop/ExecuteBudget.d.ts +48 -0
  123. package/dist/types/rop/FastTransferStream.d.ts +41 -2
  124. package/dist/types/rop/FolderTarget.d.ts +3 -2
  125. package/dist/types/rop/HandleDataCache.d.ts +145 -0
  126. package/dist/types/rop/MeetingMessageClassHandler.d.ts +23 -29
  127. package/dist/types/rop/MessageBodyStream.d.ts +16 -7
  128. package/dist/types/rop/MessageTarget.d.ts +10 -3
  129. package/dist/types/rop/NamedPropertyRegistry.d.ts +6 -2
  130. package/dist/types/rop/PropertyResolvers.d.ts +1 -1
  131. package/dist/types/rop/RepoPaging.d.ts +30 -0
  132. package/dist/types/rop/RestapiRules.d.ts +32 -0
  133. package/dist/types/rop/RopCreateMessageHandler.d.ts +1 -0
  134. package/dist/types/rop/RopDeleteFolderHandler.d.ts +36 -21
  135. package/dist/types/rop/RopDeleteMessagesHandler.d.ts +7 -0
  136. package/dist/types/rop/RopFastTransferSourceCopyPropertiesHandler.d.ts +1 -0
  137. package/dist/types/rop/RopFastTransferSourceCopyToHandler.d.ts +1 -0
  138. package/dist/types/rop/RopFastTransferSourceGetBufferHandler.d.ts +10 -11
  139. package/dist/types/rop/RopGetContentsTableHandler.d.ts +7 -13
  140. package/dist/types/rop/RopGetHierarchyTableHandler.d.ts +1 -0
  141. package/dist/types/rop/RopHandler.d.ts +40 -1
  142. package/dist/types/rop/RopOpenFolderHandler.d.ts +1 -0
  143. package/dist/types/rop/RopOpenMessageHandler.d.ts +1 -0
  144. package/dist/types/rop/RopOpenStreamHandler.d.ts +1 -0
  145. package/dist/types/rop/RopQueryRowsHandler.d.ts +3 -0
  146. package/dist/types/rop/RopReadStreamHandler.d.ts +3 -1
  147. package/dist/types/rop/RopReleaseHandler.d.ts +1 -0
  148. package/dist/types/rop/RopSaveChangesMessageHandler.d.ts +16 -1
  149. package/dist/types/rop/RopSubmitMessageHandler.d.ts +33 -23
  150. package/dist/types/rop/RopWriteStreamHandler.d.ts +18 -8
  151. package/dist/types/rop/TaskTarget.d.ts +3 -3
  152. package/dist/types/rop/TransportSend.d.ts +10 -0
  153. package/dist/types/sql/MapiEmsmdbRouteSQL.d.ts +1 -0
  154. package/package.json +3 -3
@@ -0,0 +1,30 @@
1
+ import type { RepoUtils } from "@rapidrest/service-core";
2
+ import type { ExecuteBudget } from "./ExecuteBudget.js";
3
+ /** `RepoUtils.find()`'s own largest page. */
4
+ export declare const REPO_PAGE_SIZE = 1000;
5
+ /** The most rows gathered by `findAllCapped`. */
6
+ export declare const MAX_COLLECTION_ROWS = 10000;
7
+ /** A sort order in `RepoUtils` query syntax, e.g. `{ receivedDate: "DESC", uid: "ASC" }`. Always end with a
8
+ * unique field so pages don't overlap or skip rows that tie on the leading fields. */
9
+ export type RepoSort = Record<string, "ASC" | "DESC">;
10
+ /**
11
+ * Fetches one page. `limit`/`page`/`sort` go in both the query (what the SQL query builder reads) and the options
12
+ * (what Mongo reads); without them each backend silently returns its own 100-row default in an unspecified order.
13
+ *
14
+ * With a `budget`, the query is charged before it runs and the rows it returned right after, so a request past its
15
+ * `ExecuteBudget` stops before the next query.
16
+ */
17
+ export declare function findPage<T>(repo: RepoUtils<any>, query: Record<string, any>, sort: RepoSort, page: number, pageSize?: number, budget?: ExecuteBudget): Promise<T[]>;
18
+ /**
19
+ * Fetches every row matching `query`, in `sort` order, up to `cap` rows. `truncated` is `true` when more rows may
20
+ * exist past the cap.
21
+ */
22
+ export declare function findAllCapped<T>(repo: RepoUtils<any>, query: Record<string, any>, sort: RepoSort, cap?: number, budget?: ExecuteBudget): Promise<{
23
+ items: T[];
24
+ truncated: boolean;
25
+ }>;
26
+ /**
27
+ * Fetches the rows at positions `[start, start + count)` of `query` in `sort` order, reading only the pages that
28
+ * window touches.
29
+ */
30
+ export declare function findWindow<T>(repo: RepoUtils<any>, query: Record<string, any>, sort: RepoSort, start: number, count: number, budget?: ExecuteBudget): Promise<T[]>;
@@ -0,0 +1,32 @@
1
+ import { type QueryLiteral, type RepoUtils } from "@rapidrest/service-core";
2
+ /** The longest value `boundIndexedValue()` stores verbatim - restapi's `MAX_INDEXED_VALUE_LENGTH`. */
3
+ export declare const MAX_INDEXED_VALUE_LENGTH = 255;
4
+ /**
5
+ * restapi's `boundIndexedValue`: `value` unchanged when it's at most `MAX_INDEXED_VALUE_LENGTH` characters, otherwise
6
+ * `sha256:<64 hex digits>` of its UTF-8 bytes. restapi stores `CalendarEvent.icalUid` (and `Message.messageId`/
7
+ * `conversationId`) this way, so a lookup by one of those identifiers has to bound its value the same way to match.
8
+ */
9
+ export declare function boundIndexedValue<T extends string | null | undefined>(value: T): T;
10
+ /**
11
+ * restapi's `asEntity`: `row` as an instance of `repo`'s model class. Before service-core 2.1.0, `RepoUtils.update()`
12
+ * only enforced its optimistic lock when `existing instanceof BaseEntity`, and the Mongo backend's `find()`/`findOne()`
13
+ * return plain documents, so passing one straight through turned a versioned update into an unconditional overwrite.
14
+ * 2.1.0 also locks plain documents carrying a numeric `version`, so this is now defence in depth, kept while restapi
15
+ * keeps its own copy. SQL rows are already instances and pass through as they are.
16
+ */
17
+ export declare function asEntity<T>(repo: RepoUtils<any>, row: T): T;
18
+ /**
19
+ * A `RepoUtils` query value that always matches `value` literally: service-core's `ModelUtils.literal()`, so a
20
+ * client- or sender-supplied value like `ne(x)` is never read as an operator and `me`/`null`/numeric strings are not
21
+ * substituted or coerced. Callers still compare the returned rows' field against `value` themselves.
22
+ */
23
+ export declare function literalQueryValue(value: string): QueryLiteral;
24
+ /**
25
+ * restapi `MeetingSchedulingJob`'s eligibility rule (`jobs/MeetingSchedulingJob.ts`, `processInviteCandidates`): `true`
26
+ * while the job will still send invites for `event`'s current revision, i.e. its `inviteSequenceSent` is unset or
27
+ * differs from its `sequence`. Keep in sync with that job.
28
+ */
29
+ export declare function isInvitePending(event: {
30
+ sequence?: number;
31
+ inviteSequenceSent?: number | null;
32
+ }): boolean;
@@ -19,5 +19,6 @@ import type { RopContext, RopHandler } from "./RopHandler.js";
19
19
  */
20
20
  export declare class RopCreateMessageHandler implements RopHandler {
21
21
  readonly ropId = 6;
22
+ readonly responseHandleIndexOffset = 3;
22
23
  handle(reader: BufferReader, writer: BufferWriter, context: RopContext): void;
23
24
  }
@@ -1,24 +1,33 @@
1
1
  import type { BufferReader, BufferWriter } from "../codec/BufferCursor.js";
2
2
  import type { RopContext, RopHandler } from "./RopHandler.js";
3
+ /** The deepest subfolder nesting a recursive delete walks. */
4
+ export declare const MAX_FOLDER_DEPTH = 32;
5
+ /** The most folders one recursive delete removes. */
6
+ export declare const MAX_FOLDERS_PER_DELETE = 1000;
7
+ /** The most items (messages, events, contacts, tasks) one `RopDeleteFolder` deletes. Past this the ROP stops,
8
+ * reports `PartialCompletion`, and leaves the folders that still hold items in place; the client can retry. */
9
+ export declare const MAX_ITEMS_PER_DELETE = 10000;
3
10
  /**
4
- * `RopDeleteFolder` (`[MS-OXCFOLD]`/`[MS-OXCROPS]`, RopId `0x1D`): deletes a folder by `FolderId`. Reuses
5
- * `RecoverableRepoUtils.delete()` exactly as-built in Phase 2 for `Folder`/`Message`/`CalendarEvent` - see
6
- * `RopDeleteMessagesHandler.ts`'s own doc comment for why that matters (EAS's watermark-based incremental
7
- * delete detection depends on it).
11
+ * `RopDeleteFolder` (`[MS-OXCFOLD]`/`[MS-OXCROPS]`, RopId `0x1D`): deletes a folder by `FolderId`, via
12
+ * `RecoverableRepoUtils.delete()` - see `RopDeleteMessagesHandler.ts`'s own doc comment for why that matters (EAS's
13
+ * watermark-based incremental delete detection depends on it).
8
14
  *
9
- * **Real spec semantics honored, not simplified away**: per `[MS-OXCFOLD]`, `RopDeleteFolder` only operates on
10
- * an empty folder by default - `DEL_MESSAGES` must be set to also delete the folder's own messages/calendar
11
- * events/contacts/tasks, and `DEL_FOLDERS` to also delete (recursively) its subfolders; a non-empty folder
12
- * deleted without the matching flag fails rather than silently cascading. `DELETE_HARD_DELETE` maps directly
13
- * onto `RepoDeleteOptions.purge` - real, exact semantic overlap with this library's own soft-delete model, not
14
- * a coincidence this codec papers over.
15
+ * **Real spec semantics honored**: `RopDeleteFolder` only operates on an empty folder by default - `DEL_MESSAGES`
16
+ * must be set to also delete the folder's own messages/calendar events/contacts/tasks, and `DEL_FOLDERS` to also
17
+ * delete (recursively) its subfolders; a non-empty folder deleted without the matching flag fails rather than
18
+ * silently cascading.
15
19
  *
16
- * **Contacts/Tasks folders**: emptiness is checked against `contactRepo`/`taskRepo` alongside `messageRepo`/
17
- * `calendarEventRepo`, and both are included in the delete cascade - a `CONTACTS`/`TASKS` folder's real content
18
- * lives in those repos (see `ContactTarget.ts`/`TaskTarget.ts`), not `messageRepo`, so checking only the latter
19
- * would let a non-empty Contacts/Tasks folder be deleted without `DEL_MESSAGES` and orphan its rows. Guarded
20
- * with `context.contactRepo`/`taskRepo` presence checks the same way `RopGetContentsTableHandler` does, since
21
- * `RopContext`'s doc comment documents both as optional.
20
+ * **Always a soft delete.** `DELETE_HARD_DELETE` is ignored. A permanent delete has to go through restapi's legal
21
+ * hold check first (the REST routes call `LegalHoldUtils.assertNotOnLegalHold()` for `purge=true`), and that helper
22
+ * isn't part of `@rapidmx/restapi`'s public exports. A soft-deleted item stays recoverable and discoverable, which is
23
+ * exactly what a hold requires, so MAPI never purges. Each deleted message is audited as `MESSAGE_DELETE`, like the
24
+ * REST message route.
25
+ *
26
+ * **Bounded.** The subfolder tree is walked iteratively with a visited set (a corrupt `parentFolderUid` cycle
27
+ * can't loop), at most `MAX_FOLDER_DEPTH` deep and `MAX_FOLDERS_PER_DELETE` wide, and only through folders in the
28
+ * caller's own mailbox. Items are fetched in explicit, paged queries (a bare `find()` stops at 100 rows, which used
29
+ * to leave items behind in a folder that was then deleted), up to `MAX_ITEMS_PER_DELETE`. Every query and delete is
30
+ * charged to the request's `ExecuteBudget` before it runs, so repeating this ROP can't multiply the walk unbounded.
22
31
  *
23
32
  * The request's own `InputHandleIndex` (nominally the *parent* folder of the one being deleted) is validated to
24
33
  * be a real, open folder handle but not cross-checked against the target folder's actual `parentFolderUid` -
@@ -30,9 +39,15 @@ import type { RopContext, RopHandler } from "./RopHandler.js";
30
39
  export declare class RopDeleteFolderHandler implements RopHandler {
31
40
  readonly ropId = 29;
32
41
  handle(reader: BufferReader, writer: BufferWriter, context: RopContext): Promise<void>;
33
- /** Cascades a `DEL_FOLDERS` delete into `folderUid`'s own messages/calendar events/subfolders before
34
- * deleting it - this data model's folder hierarchy is a plain parent-pointer tree (never a graph), so
35
- * unbounded recursion here needs no cycle protection, the same assumption `FolderTarget.resolveFolderChildren`
36
- * already makes. */
37
- private deleteFolderRecursive;
42
+ private writeFailure;
43
+ /** The repos a folder's items can live in, skipping the optional ones the context doesn't have. */
44
+ private itemRepos;
45
+ private hasItems;
46
+ /**
47
+ * `rootUid` and every folder below it in the caller's mailbox, children before their parents, or `undefined` when
48
+ * the tree is deeper than `MAX_FOLDER_DEPTH` or has more than `MAX_FOLDERS_PER_DELETE` folders.
49
+ */
50
+ private collectSubtree;
51
+ /** Soft-deletes `folderUid`'s items within `budget`. Returns `false` if items remain once the budget is spent. */
52
+ private deleteItems;
38
53
  }
@@ -1,5 +1,12 @@
1
1
  import type { BufferReader, BufferWriter } from "../codec/BufferCursor.js";
2
2
  import type { RopContext, RopHandler } from "./RopHandler.js";
3
+ /** Audits a message deleted through MAPI with the same `MESSAGE_DELETE` entry the REST message route records. */
4
+ export declare function auditMessageDelete(context: RopContext, message: {
5
+ uid: string;
6
+ mailboxUid: string;
7
+ subject?: string;
8
+ folderUid: string;
9
+ }): Promise<void>;
3
10
  /**
4
11
  * `RopDeleteMessages` (`[MS-OXCMSG]`/`[MS-OXCROPS]`, RopId `0x1E`): deletes one or more messages by `MessageId`
5
12
  * from an already-open folder handle. Reuses `RecoverableRepoUtils.delete()` exactly as-built in Phase 2 - the
@@ -20,5 +20,6 @@ import type { RopContext, RopHandler } from "./RopHandler.js";
20
20
  */
21
21
  export declare class RopFastTransferSourceCopyPropertiesHandler implements RopHandler {
22
22
  readonly ropId = 105;
23
+ readonly responseHandleIndexOffset = 3;
23
24
  handle(reader: BufferReader, writer: BufferWriter, context: RopContext): Promise<void>;
24
25
  }
@@ -20,5 +20,6 @@ import type { RopContext, RopHandler } from "./RopHandler.js";
20
20
  */
21
21
  export declare class RopFastTransferSourceCopyToHandler implements RopHandler {
22
22
  readonly ropId = 77;
23
+ readonly responseHandleIndexOffset = 3;
23
24
  handle(reader: BufferReader, writer: BufferWriter, context: RopContext): Promise<void>;
24
25
  }
@@ -3,21 +3,20 @@ import type { RopContext, RopHandler } from "./RopHandler.js";
3
3
  /**
4
4
  * `RopFastTransferSourceGetBuffer` (`[MS-OXCFXICS]`/`[MS-OXCROPS]`, RopId `0x4E`): pages the FastTransfer
5
5
  * stream a prior `RopFastTransferSourceCopyTo`/`CopyProperties` built (`FastTransferStream.ts`) out of its
6
- * `"fastTransfer"` handle, `BufferSize` bytes at a time (or the entire remaining buffer, capped to
7
- * `MAX_TRANSFER_BUFFER_SIZE`, for the `0xBABE` "server-determined" sentinel - `MaximumBufferSize`, present only
8
- * in that case, is decoded to advance the reader correctly but not honored, since honoring it would mean
9
- * returning *more* than one chunk can carry, not less). Reports `Done` once the whole buffer has been returned
10
- * across one or more calls, `Partial` otherwise - `NoRoom` and `Error` are never produced (a `"fastTransfer"`
11
- * handle, once created, always has a complete, already-valid buffer to page from).
6
+ * `"fastTransfer"` handle, `BufferSize` bytes at a time. For the `0xBABE` "server-determined" sentinel the server
7
+ * picks the size, up to the client's `MaximumBufferSize`.
12
8
  *
13
- * `BackoffTime` is never emitted (this pragmatic subset never returns the one `ReturnValue` that field is
14
- * conditional on), and the failure path (`ERROR_INVALID_OBJECT`) omits every field after `ReturnValue` entirely
15
- * - the same "just the fixed header, no success-only tail" shape every other failing handler in this pragmatic
16
- * subset already uses, not a specific claim about the real spec's own error-path byte layout.
9
+ * Every chunk is also held to the room left in this request's ROP output buffer (`context.ropOutputRemaining`) and
10
+ * the 16-bit `TransferBufferSize`. Reports `Done` once the whole stream has been returned, `Partial` otherwise, and
11
+ * `NoRoom` when the output buffer has no space for any bytes this time (the client asks again in a new request).
12
+ *
13
+ * Fails with `MAPI_E_TOO_BIG` when a stream that had to be rebuilt is too large, and with `MAPI_E_CALL_FAILED` when
14
+ * the stream was lost part-way through (see `loadFastTransferBuffer`) - never with bytes from a different stream.
15
+ * Failure responses carry only `ReturnValue`, like every other failing handler here. `BackoffTime` is never emitted.
17
16
  *
18
17
  * @author Jean-Philippe Steinmetz
19
18
  */
20
19
  export declare class RopFastTransferSourceGetBufferHandler implements RopHandler {
21
20
  readonly ropId = 78;
22
- handle(reader: BufferReader, writer: BufferWriter, context: RopContext): void;
21
+ handle(reader: BufferReader, writer: BufferWriter, context: RopContext): Promise<void>;
23
22
  }
@@ -5,27 +5,21 @@ import type { RopContext, RopHandler } from "./RopHandler.js";
5
5
  * messages, producing a new `"table"` Server object handle `RopSetColumns`/`RopQueryRows` reference by
6
6
  * `InputHandleIndex` - the exact same request/response shape as `RopGetHierarchyTable` (confirmed identical
7
7
  * field-by-field against `[MS-OXCROPS]`'s own request-buffer page, differing only in `RopId`), just listing
8
- * messages instead of child folders. `RopSetColumns`/`RopQueryRows` themselves need no changes at all to
9
- * support this - they already operate generically on whatever `rows`/`columns` a table handle carries,
10
- * dispatching to `FolderTarget`'s or `MessageTarget`'s resolvers by row-target prefix (`"folder:"`/`"virtual:"`
11
- * vs `"message:"`).
8
+ * messages instead of child folders.
9
+ *
10
+ * The table holds no rows. It records which kind of item the folder contains (`contentsKind`, decided once here
11
+ * from the folder's own `type`: Calendar folders list `"calendarEvent:<uid>"` rows, Contacts `"contact:<uid>"`,
12
+ * Tasks `"task:<uid>"`, everything else `"message:<uid>"`), and `RopQueryRows` reads each requested window from
13
+ * the database - see `ContentsTable.ts`.
12
14
  *
13
15
  * A folder's contents table is only ever non-empty for a real folder (`"folder:<uid>"`) - the virtual special
14
16
  * folders (`RopLogonHandler`'s own doc comment) have no real backing row to hold messages under, so opening a
15
17
  * contents table on one always yields an empty table rather than an error.
16
18
  *
17
- * **Calendar folders** (`Folder.type === FolderType.CALENDAR`) list `"calendarEvent:<uid>"` rows resolved via
18
- * `calendarEventRepo` instead of `"message:<uid>"` rows - a single table handle only ever holds one kind of
19
- * row, decided once here by checking the folder's own `type`, exactly the same "resolve by target-string
20
- * prefix downstream" design `MessageTarget`/`FolderTarget` already use for their own rows. **Contacts and
21
- * Tasks** folders (`FolderType.CONTACTS`/`TASKS`) list `"contact:<uid>"`/`"task:<uid>"` rows the same way, via
22
- * `contactRepo`/`taskRepo` - empty (rather than an error) when the context has neither wired up, the same
23
- * optional-repo degradation `RopContext`'s own doc comment describes.
24
- *
25
19
  * @author Jean-Philippe Steinmetz
26
20
  */
27
21
  export declare class RopGetContentsTableHandler implements RopHandler {
28
22
  readonly ropId = 5;
23
+ readonly responseHandleIndexOffset = 3;
29
24
  handle(reader: BufferReader, writer: BufferWriter, context: RopContext): Promise<void>;
30
- private resolveRows;
31
25
  }
@@ -13,5 +13,6 @@ import type { RopContext, RopHandler } from "./RopHandler.js";
13
13
  */
14
14
  export declare class RopGetHierarchyTableHandler implements RopHandler {
15
15
  readonly ropId = 4;
16
+ readonly responseHandleIndexOffset = 3;
16
17
  handle(reader: BufferReader, writer: BufferWriter, context: RopContext): Promise<void>;
17
18
  }
@@ -1,8 +1,10 @@
1
1
  import type { RepoUtils } from "@rapidrest/service-core";
2
- import type { BlobStore } from "@rapidmx/restapi";
2
+ import type { AuditLogParams, BlobStore } from "@rapidmx/restapi";
3
3
  import type { ScanPipeline } from "@rapidmx/restapi/scan";
4
4
  import type { BufferReader, BufferWriter } from "../codec/BufferCursor.js";
5
5
  import type { MapiSessionContext } from "../MapiSessionManager.js";
6
+ import type { ExecuteBudget } from "./ExecuteBudget.js";
7
+ import { type HandleDataStore } from "./HandleDataCache.js";
6
8
  /**
7
9
  * Everything a `RopHandler` needs beyond the raw ROP bytes it decodes itself. `folderRepo`/`mailboxRepo` are
8
10
  * built once by `BaseMapiEmsmdbRoute` (not by each handler, unlike EAS's per-command repo pattern - MAPI's ROPs
@@ -52,7 +54,36 @@ export interface RopContext {
52
54
  blobStore: BlobStore;
53
55
  scanPipeline: ScanPipeline;
54
56
  mailTransport: any;
57
+ /** Records an audit log entry as the calling user, the same way the REST routes do. Optional like the repos
58
+ * above: absent when the route has no audit log class configured, in which case nothing is recorded. */
59
+ audit?: (params: AuditLogParams) => Promise<void>;
60
+ /** Recomputes, re-caches and publishes the given folders' `unreadCount`/`totalCount` - restapi's
61
+ * `refreshFolderCounts()` (`@rapidmx/restapi`), bound to this route's own `messageRepo`/`folderRepo`/`folderClass`/
62
+ * `notificationUtils`. `Folder.unreadCount`/`totalCount` are a cache restapi's own writes keep fresh (see
63
+ * `FolderCountUtils.ts`'s module doc comment); a handler that adds, deletes or moves a `Message` row through
64
+ * `context.messageRepo` itself - never through a `BaseFolderRoute`/`BaseMessageRoute` this library already refreshes
65
+ * after - must call this for every folder it touched, or Outlook's own folder pane (which reads
66
+ * `PidTagContentCount`/`PidTagContentUnreadCount` straight off the stored fields, not derived on read - see
67
+ * `PropertyResolvers.ts`/`FolderTarget.ts`) goes stale. `bumpSyncKey` should be `true` when a message was added to a
68
+ * folder (restapi's own convention - see `refreshFolderCounts()`'s doc comment), and left unset for a plain
69
+ * deletion or status change. Always present in real use (`BaseMapiEmsmdbRoute.dispatch()` populates it
70
+ * unconditionally); optional here only so existing `RopContext`-literal test fixtures that predate this need no
71
+ * new field, the same reasoning `contactRepo`/`taskRepo`/`labelRepo` above already documents. */
72
+ notifyFolderCounts?: (folderUids: Iterable<string | undefined | null>, options?: {
73
+ bumpSyncKey?: boolean;
74
+ }) => Promise<void>;
75
+ /** Shared storage for FastTransfer streams and write-stream chunks. Absent in unit tests, where
76
+ * `defaultHandleDataStore` (this process only) is used - see `handleDataStoreOf`. */
77
+ handleData?: HandleDataStore;
78
+ /** This request's work budget, set by `dispatchRops`. Absent when a handler runs on its own. */
79
+ budget?: ExecuteBudget;
80
+ /** The bytes still free in this request's ROP output buffer for the current ROP's response, set by
81
+ * `dispatchRops` before each ROP. Handlers returning variable-size data (`RopReadStream`,
82
+ * `RopFastTransferSourceGetBuffer`, `RopQueryRows`) shrink their response to fit. Absent means unlimited. */
83
+ ropOutputRemaining?: number;
55
84
  }
85
+ /** The `HandleDataStore` a handler should use for `context`. */
86
+ export declare function handleDataStoreOf(context: Pick<RopContext, "handleData">): HandleDataStore;
56
87
  /**
57
88
  * One class per ROP, mirroring `EasCommandHandler`'s exact one-class-per-command convention. A handler owns
58
89
  * its own request/response wire format entirely - decoding whatever fields follow its own `RopId` byte from
@@ -63,5 +94,13 @@ export interface RopContext {
63
94
  export interface RopHandler {
64
95
  /** The `RopId` byte this handler processes (e.g. `0xFE` for `RopLogon`). */
65
96
  readonly ropId: number;
97
+ /** Where the handle index a failure response echoes sits in the request, counting the `RopId` byte as 0. Most
98
+ * ROPs echo `InputHandleIndex` (2, the default); ROPs that create an object echo `OutputHandleIndex` (3). Used
99
+ * by `RopDispatcher` to write a failure response when the handler throws. */
100
+ readonly responseHandleIndexOffset?: number;
101
+ /** Zero bytes a failure response carries after `ReturnValue` (`RopReadStream`'s `DataSize`, ...). */
102
+ readonly failureTailBytes?: number;
103
+ /** `true` for a ROP that never has a response (`RopRelease`), so a failure writes nothing either. */
104
+ readonly hasNoResponse?: boolean;
66
105
  handle(reader: BufferReader, writer: BufferWriter, context: RopContext): Promise<void> | void;
67
106
  }
@@ -15,5 +15,6 @@ import type { RopContext, RopHandler } from "./RopHandler.js";
15
15
  */
16
16
  export declare class RopOpenFolderHandler implements RopHandler {
17
17
  readonly ropId = 2;
18
+ readonly responseHandleIndexOffset = 3;
18
19
  handle(reader: BufferReader, writer: BufferWriter, context: RopContext): void;
19
20
  }
@@ -23,5 +23,6 @@ import type { RopContext, RopHandler } from "./RopHandler.js";
23
23
  */
24
24
  export declare class RopOpenMessageHandler implements RopHandler {
25
25
  readonly ropId = 3;
26
+ readonly responseHandleIndexOffset = 3;
26
27
  handle(reader: BufferReader, writer: BufferWriter, context: RopContext): Promise<void>;
27
28
  }
@@ -23,5 +23,6 @@ import type { RopContext, RopHandler } from "./RopHandler.js";
23
23
  */
24
24
  export declare class RopOpenStreamHandler implements RopHandler {
25
25
  readonly ropId = 43;
26
+ readonly responseHandleIndexOffset = 3;
26
27
  handle(reader: BufferReader, writer: BufferWriter, context: RopContext): Promise<void>;
27
28
  }
@@ -1,5 +1,8 @@
1
1
  import { BufferReader, BufferWriter } from "../codec/BufferCursor.js";
2
2
  import type { RopContext, RopHandler } from "./RopHandler.js";
3
+ /** The most rows one `RopQueryRows` returns. Fewer rows than requested is normal table paging: the client asks
4
+ * again from the new cursor position. */
5
+ export declare const MAX_ROWS_PER_QUERY = 500;
3
6
  /**
4
7
  * `RopQueryRows` (`[MS-OXCTABL]`/`[MS-OXCROPS]`): fetches up to `RowCount` rows from an already-configured
5
8
  * table (`RopGetHierarchyTable` + `RopSetColumns`), advancing the table's cursor. Confirmed field-by-field
@@ -5,7 +5,8 @@ import type { RopContext, RopHandler } from "./RopHandler.js";
5
5
  * `ByteCount` is the `0xBABE` sentinel) bytes from an already-opened stream (`RopOpenStream`), advancing the
6
6
  * stream's read position - clamped to `MAX_DATA_SIZE` (`0xFFFF`) regardless of what the client asked for, since
7
7
  * the response's own `DataSize` field can't carry more than that; a client wanting more of a large body simply
8
- * calls `RopReadStream` again from the advanced `streamPosition`, the same paging behavior real Exchange uses.
8
+ * calls `RopReadStream` again from the advanced `streamPosition`, the same paging behavior real Exchange uses. It is
9
+ * likewise held to the room left in the request's ROP output buffer (`context.ropOutputRemaining`).
9
10
  * Unlike most other ROPs in this pragmatic subset, `[MS-OXCROPS]` documents only one combined response-buffer
10
11
  * shape for this ROP (no separate Success/Failure pages) - `DataSize`/`Data` are always present, `DataSize = 0`
11
12
  * (empty `Data`) standing in for the failure case rather than the fields being omitted entirely.
@@ -14,5 +15,6 @@ import type { RopContext, RopHandler } from "./RopHandler.js";
14
15
  */
15
16
  export declare class RopReadStreamHandler implements RopHandler {
16
17
  readonly ropId = 44;
18
+ readonly failureTailBytes = 2;
17
19
  handle(reader: BufferReader, writer: BufferWriter, context: RopContext): Promise<void>;
18
20
  }
@@ -10,5 +10,6 @@ import type { RopContext, RopHandler } from "./RopHandler.js";
10
10
  */
11
11
  export declare class RopReleaseHandler implements RopHandler {
12
12
  readonly ropId = 1;
13
+ readonly hasNoResponse = true;
13
14
  handle(reader: BufferReader, _writer: BufferWriter, context: RopContext): void;
14
15
  }
@@ -1,3 +1,4 @@
1
+ import { type CalendarEvent } from "@rapidmx/restapi";
1
2
  import { BufferReader, type BufferWriter } from "../codec/BufferCursor.js";
2
3
  import type { RopContext, RopHandler } from "./RopHandler.js";
3
4
  /**
@@ -25,7 +26,11 @@ import type { RopContext, RopHandler } from "./RopHandler.js";
25
26
  * `"calendarEvent:<uid>"`) a real `CalendarEvent` row, so a subsequent `RopGetPropertiesSpecific` can read it
26
27
  * back before `RopSubmitMessage` is ever called (a calendar item must exist as soon as it's saved, the same way
27
28
  * a real Exchange server behaves - unlike a mail draft, which this pragmatic subset never persists at all until
28
- * Submit).
29
+ * Submit). Saving never invites anyone: a saved revision is stamped as already handled (`inviteSequenceSent =
30
+ * sequence`), so restapi's `MeetingSchedulingJob` skips it until `RopSubmitMessageHandler.submitAppointment` asks for
31
+ * invites. Attendees of a submitted meeting are then mailed by that job, which is why a meeting is capped at
32
+ * `MAX_RECIPIENTS_PER_MESSAGE` attendees here. Client-supplied times and reminder minutes that don't parse are ignored (the field keeps its
33
+ * existing or default value) rather than stored as `Invalid Date`/`NaN`, and an update is versioned (`asEntity`).
29
34
  *
30
35
  * @author Jean-Philippe Steinmetz
31
36
  */
@@ -39,6 +44,16 @@ export declare class RopSaveChangesMessageHandler implements RopHandler {
39
44
  * below - the write-side mirror of `PropertyResolvers.calendarEventValueFor`'s read-side switch. A field the
40
45
  * client never set (e.g. no `RopSetProperties` call touched `PidLidBusyStatus`) falls back to the existing
41
46
  * row's own value on an update, or the entity class's own default on a fresh create.
47
+ *
48
+ * Returns `undefined`, saving nothing, for more than `MAX_RECIPIENTS_PER_MESSAGE` attendees: restapi's
49
+ * `MeetingSchedulingJob` mails every attendee of a saved meeting.
42
50
  */
43
51
  private saveAppointment;
44
52
  }
53
+ /**
54
+ * `true` when an update changes what attendees were invited to: the start or end time, location, recurrence, or
55
+ * who is invited. The same fields restapi's `BaseCalendarEventRoute.update()` bumps `sequence` for (a MAPI save
56
+ * never sets `status`), so an edit made in Outlook is re-sent to attendees and supersedes their copy (RFC 5546
57
+ * `SEQUENCE`) exactly like one made through REST.
58
+ */
59
+ export declare function isSchedulingRelevantChange(existing: Pick<CalendarEvent, "startDate" | "endDate" | "location" | "recurrenceRule" | "attendees">, updated: Pick<CalendarEvent, "startDate" | "endDate" | "location" | "recurrenceRule" | "attendees">): boolean;
@@ -1,10 +1,6 @@
1
1
  import type { BufferReader, BufferWriter } from "../codec/BufferCursor.js";
2
- import type { RopContext, RopHandler } from "./RopHandler.js";
3
- /** Splits a `PidTagDisplayTo`/`Cc`/`Bcc`-style string on the semicolons real Outlook separates recipients
4
- * with (also tolerating commas, in case a client or test harness uses that convention instead), trimming and
5
- * dropping empty entries. Exported since `RopSaveChangesMessageHandler.ts` reads the same accumulated
6
- * `PidTagDisplayTo`/`Cc` draft properties to build a Calendar item's attendee list. */
7
- export declare function parseAddressList(value: string | undefined): string[];
2
+ import { type RopContext, type RopHandler } from "./RopHandler.js";
3
+ export { isPlainEmailAddress, parseAddressList } from "./AddressList.js";
8
4
  /**
9
5
  * `RopSubmitMessage` (`[MS-OXOMSG]`/`[MS-OXCROPS]`): sends a composed message - the actual trigger point for
10
6
  * this pragmatic subset's compose/send pipeline, since `RopSaveChangesMessage` itself does no real persistence
@@ -22,12 +18,12 @@ export declare function parseAddressList(value: string | undefined): string[];
22
18
  * pragmatic subset's ROP coverage doesn't include. Instead, addressing is read from `PidTagDisplayTo`/
23
19
  * `DisplayCc`/`DisplayBcc` - the same semicolon-separated "cached recipient display string" properties real
24
20
  * Outlook *also* always sets via `RopSetProperties` alongside `RopModifyRecipients` (as a display-only
25
- * convenience cache). This works correctly for the common case of composing to bare, unresolved email
26
- * addresses (nothing to resolve to a distinct display name) - the case this library's own integration tests
27
- * exercise - but is **not** a substitute for real recipient rows: a client that resolves typed names against an
28
- * address book before submitting would populate these fields with display names, not addresses, and this
29
- * pragmatic subset has no way to recover a real SMTP address from a bare display name. Documented here rather
30
- * than silently producing wrong addressing.
21
+ * convenience cache). Entries are separated by `;` and parsed with nodemailer's `addressparser`, so both bare
22
+ * addresses and resolved `Name <address>` forms work. A client that resolved a name against an address book may
23
+ * write the display name alone; that is looked up among the caller's own contacts (see `resolveRecipientList`), and
24
+ * a name matching no single contact fails the submit with `MAPI_E_NOT_FOUND`. It is still **not** a substitute for
25
+ * real recipient rows (there is no recipient table to fall back on), so the whole send is refused rather than sent to
26
+ * fewer people than the user addressed. A malformed recipient fails with `MAPI_E_INVALID_PARAMETER`.
31
27
  *
32
28
  * `SubmitFlags` (`PreprocessOnly`, ...) is decoded to advance past it correctly but not honored - this
33
29
  * pragmatic subset has no transport-agent preprocessing distinction to vary by flag.
@@ -40,6 +36,12 @@ export declare function parseAddressList(value: string | undefined): string[];
40
36
  * own `requestReceipt` field - honored only when the client explicitly sets it, not this mailbox's
41
37
  * `alwaysRequestReceiptInternal`/`External` defaults (those apply to the REST/webmail compose path only).
42
38
  *
39
+ * **Once per draft.** A mail or meeting-response draft is marked `submitted` as soon as a submit gets past the handle
40
+ * check, whatever the outcome, and a submitted draft can't be submitted again (`MAPI_E_INVALID_OBJECT`) or changed by
41
+ * `RopSetProperties`/`RopWriteStream`; its write-stream chunks are deleted once read. Each `Execute` runs at most
42
+ * `MAX_SUBMITS_PER_EXECUTE` submits, and a message may have at most `MAX_RECIPIENTS_PER_MESSAGE` recipients
43
+ * (`MAPI_E_TOO_BIG`), counted before any display name is looked up.
44
+ *
43
45
  * **Calendar branches**: a draft whose `PidTagMessageClass` starts with `"IPM.Appointment"` is routed to
44
46
  * `submitAppointment()` instead of the mail path below - see that method's own doc comment. One starting with
45
47
  * `"IPM.Schedule.Meeting.Resp."` (an attendee's own accept/decline/tentative response to a meeting this server
@@ -52,19 +54,27 @@ export declare class RopSubmitMessageHandler implements RopHandler {
52
54
  readonly ropId = 50;
53
55
  handle(reader: BufferReader, writer: BufferWriter, context: RopContext): Promise<void>;
54
56
  /**
55
- * Submits a Calendar item. The appointment itself is already persisted (`RopSaveChangesMessageHandler`
56
- * writes the real `CalendarEvent` row at Save time, unlike a mail draft, which stays purely in-session until
57
- * Submit) - this method's only job is notifying attendees, if any were set (via `PidTagDisplayTo`/`Cc`,
58
- * decoded into `CalendarEvent.attendees` at Save time). No attendees means no invite to send (e.g. a private,
59
- * non-meeting appointment) - a well-formed success response either way, since the appointment already exists.
57
+ * Submits a Calendar item. The appointment itself is already persisted (`RopSaveChangesMessageHandler` writes the
58
+ * real `CalendarEvent` row at Save time, unlike a mail draft, which stays purely in-session until Submit), so there
59
+ * is nothing left to store and nothing is sent from here.
60
60
  *
61
- * Unlike ordinary mail, there is no separate "Sent Items copy" to create - the appointment's durable copy
62
- * *is* the `CalendarEvent` row already sitting in the organizer's own Calendar folder.
61
+ * **Invites are restapi's `MeetingSchedulingJob`'s job.** That job sends an iTIP `REQUEST` for every
62
+ * organizer-owned event whose `inviteSequenceSent` differs from its `sequence`, claiming each revision with a
63
+ * versioned update before sending and building the invite with restapi's own `buildEventIcs` (so a recurring
64
+ * meeting's invite carries its `RRULE` and exceptions). Saving stamps `inviteSequenceSent = sequence`, so a meeting
65
+ * that is only saved ("Save & Close", or "save changes but don't send") invites no one. Submitting clears the stamp
66
+ * (`inviteSequenceSent: null`) with a versioned update, which makes the job send the current revision exactly once.
67
+ * - A revision that is already waiting for the job (submitted and not yet sent, or never stamped) is left alone, so
68
+ * submitting it again before the job runs doesn't cause a second invite.
69
+ * - The update is versioned (`asEntity`). The job only ever claims a waiting revision, so a conflict here comes from
70
+ * some other edit (an attendee's reply updating the row, a REST edit); the row is read again and the check repeated,
71
+ * up to `MAX_INVITE_REQUEST_ATTEMPTS` times, after which the ROP fails (`MAPI_E_CALL_FAILED`).
72
+ * - Submitting a revision the job has already sent sends it again: the client asked to send.
63
73
  *
64
- * The invite itself is a minimal iCalendar (`RFC 5545`) `VEVENT` with `METHOD:REQUEST` (`RFC 5546`),
65
- * attached via `nodemailer`'s own `MailComposer` `icalEvent` option (which produces both a `text/calendar;
66
- * method=REQUEST` MIME alternative and an `.ics` attachment - the standard dual form real invite emails use)
67
- * - reusing the identical `scanAndRelay()` pipeline the mail path above already calls.
74
+ * Only the organizer's own copy may be submitted (an attendee's copy of someone else's meeting answers
75
+ * `MAPI_E_INVALID_OBJECT`, as before, and nothing is changed).
68
76
  */
69
77
  private submitAppointment;
70
78
  }
79
+ /** How many times `requestInvites` re-reads a row whose versioned update lost to another edit before giving up. */
80
+ export declare const MAX_INVITE_REQUEST_ATTEMPTS = 3;
@@ -1,14 +1,20 @@
1
1
  import type { BufferReader, BufferWriter } from "../codec/BufferCursor.js";
2
- import type { RopContext, RopHandler } from "./RopHandler.js";
2
+ import type { MapiObjectHandle } from "../MapiSessionManager.js";
3
+ import { type RopContext, type RopHandler } from "./RopHandler.js";
4
+ /** The largest body a write stream accumulates. */
5
+ export declare const MAX_WRITE_STREAM_BYTES: number;
6
+ /** How long a write stream's chunks are kept after its last write - the session's own idle lifetime. */
7
+ export declare const WRITE_STREAM_TTL_SECONDS: number;
8
+ export { writeStreamKey } from "./HandleDataCache.js";
3
9
  /**
4
10
  * `RopWriteStream` (`[MS-OXCPRPT]`/`[MS-OXCROPS]`): writes bytes to a stream opened in `ReadWrite`/`Create`
5
11
  * mode (`RopOpenStreamHandler`'s write-mode branch - the only kind of writable stream this pragmatic subset
6
- * ever produces, always a `RopCreateMessage` draft's `PidTagBody`). Accumulates `Data` into the stream handle's
7
- * `writeBufferBase64` field, decode-concat-reencode each call rather than a naive string concatenation of
8
- * per-call base64 chunks - base64 is not concatenation-safe (a chunk's padding only belongs at the true end of
9
- * the data), so reassembling the real bytes requires decoding what's accumulated so far, appending the new raw
10
- * bytes, and re-encoding the whole thing. `RopSaveChangesMessageHandler`/`RopSubmitMessageHandler` decode this
11
- * buffer back to the final body text once composing is complete.
12
+ * ever produces, always a `RopCreateMessage` draft's `PidTagBody`). Each write is stored as a chunk keyed by its
13
+ * offset in the `HandleDataStore` (Redis when configured), not in the session: only `writeSize` is session state.
14
+ * Keying by offset makes a retried write (after a request whose session save lost a conflict) replace its chunk
15
+ * instead of appending it twice. Up to `MAX_WRITE_STREAM_BYTES`; a write past that fails with `MAPI_E_TOO_BIG` and
16
+ * nothing is stored, as it is when the session's or user's handle data quota (`handleDataOwners`) is used up. A stream
17
+ * whose draft has already been submitted takes no more writes. `RopSubmitMessageHandler` reassembles the body (`readWriteStream`) once composing is complete.
12
18
  *
13
19
  * Like `RopReadStream`, `[MS-OXCROPS]` documents only one combined response-buffer shape for this ROP (no
14
20
  * separate Success/Failure pages) - `WrittenSize` is always present, `0` standing in for the failure case.
@@ -17,5 +23,9 @@ import type { RopContext, RopHandler } from "./RopHandler.js";
17
23
  */
18
24
  export declare class RopWriteStreamHandler implements RopHandler {
19
25
  readonly ropId = 45;
20
- handle(reader: BufferReader, writer: BufferWriter, context: RopContext): void;
26
+ readonly failureTailBytes = 2;
27
+ handle(reader: BufferReader, writer: BufferWriter, context: RopContext): Promise<void>;
21
28
  }
29
+ /** The bytes written so far to the write stream `stream` at `handleIndex`, or `undefined` when they can no longer be
30
+ * reassembled (a chunk expired or was evicted). */
31
+ export declare function readWriteStream(context: RopContext, handleIndex: number, stream: MapiObjectHandle): Promise<Buffer | undefined>;
@@ -1,4 +1,5 @@
1
1
  import type { RepoUtils } from "@rapidrest/service-core";
2
+ import type { RepoSort } from "./RepoPaging.js";
2
3
  /**
3
4
  * The `RopGetContentsTable`/`RopOpenMessage` analog of `MessageTarget.ts`, for a `Folder` of type `TASKS`:
4
5
  * resolves a `"task:<uid>"` row/handle target into the display data a task-item row or an opened task's
@@ -19,6 +20,5 @@ export interface TaskTargetInfo {
19
20
  * (soft-deleted or otherwise) - the same "don't fail the whole ROP over one stale row" principle
20
21
  * `MessageTarget.resolveMessageInfo`/`ContactTarget.resolveContactInfo` already apply. */
21
22
  export declare function resolveTaskInfo(target: string, taskRepo: RepoUtils<any>): Promise<TaskTargetInfo>;
22
- /** Resolves the tasks directly in `folderUid`, as `"task:<uid>"` target strings, for `RopGetContentsTable`
23
- * against a `TASKS`-type folder. */
24
- export declare function resolveFolderTasks(folderUid: string, taskRepo: RepoUtils<any>): Promise<string[]>;
23
+ /** The order a `TASKS` folder's contents table lists tasks in (see `ContentsTable.ts`). */
24
+ export declare const TASK_SORT: RepoSort;
@@ -0,0 +1,10 @@
1
+ import type { MailTransport, OutboundMessage, TransportResult } from "@rapidmx/restapi";
2
+ /**
3
+ * Sends `message` and throws unless the transport accepted it for at least one recipient and rejected none.
4
+ *
5
+ * The same rule as restapi's own `sendOrThrow()` (`transport/TransportResultUtils.ts`), which isn't exported from
6
+ * the `@rapidmx/restapi` package root, so it can't be imported here; keep the two in sync. Every bundled transport
7
+ * reports a failed relay through `TransportResult.rejected` rather than throwing, so a bare `send()` would treat a
8
+ * rejected message as sent.
9
+ */
10
+ export declare function sendOrThrow(transport: MailTransport, message: OutboundMessage): Promise<TransportResult>;
@@ -15,5 +15,6 @@ export declare class MapiEmsmdbRouteSQL extends BaseMapiEmsmdbRoute<MailboxSQL>
15
15
  protected contactClass: any;
16
16
  protected taskClass: any;
17
17
  protected labelClass: any;
18
+ protected auditLogClass: any;
18
19
  protected ropHandlerClasses: any[];
19
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rapidmx/mapi-plugin",
3
- "version": "1.0.0-beta.3",
3
+ "version": "1.0.0-beta.5",
4
4
  "description": "A RapidMX plugin that provides MAPI over HTTP protocol support.",
5
5
  "repository": "https://github.com/RapidMX/mapi.git",
6
6
  "author": "RapidREST <rapidrests@gmail.com>",
@@ -31,9 +31,9 @@
31
31
  "nodemailer": "^10.0.1"
32
32
  },
33
33
  "devDependencies": {
34
- "@rapidmx/restapi": "^0.9.0",
34
+ "@rapidmx/restapi": "^0.17.0",
35
35
  "@rapidrest/core": "^5.2.2",
36
- "@rapidrest/service-core": "^2.0.0",
36
+ "@rapidrest/service-core": "^2.1.0",
37
37
  "@swc/core": "^1.16.2",
38
38
  "@types/mailparser": "^3.4.6",
39
39
  "@types/nconf": "^0.10.7",