@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.
- package/README.md +4 -4
- package/dist/lib/BaseMapiEmsmdbRoute.js +229 -37
- package/dist/lib/BaseMapiEmsmdbRoute.js.map +1 -1
- package/dist/lib/MapiSessionManager.js +404 -48
- package/dist/lib/MapiSessionManager.js.map +1 -1
- package/dist/lib/RegexPatternUtils.js +30 -0
- package/dist/lib/RegexPatternUtils.js.map +1 -0
- package/dist/lib/RopDispatcher.js +130 -19
- package/dist/lib/RopDispatcher.js.map +1 -1
- package/dist/lib/codec/AppointmentRecurrence.js +22 -21
- package/dist/lib/codec/AppointmentRecurrence.js.map +1 -1
- package/dist/lib/codec/BufferCursor.js +41 -12
- package/dist/lib/codec/BufferCursor.js.map +1 -1
- package/dist/lib/codec/GlobalObjectId.js +26 -20
- package/dist/lib/codec/GlobalObjectId.js.map +1 -1
- package/dist/lib/codec/MapiTimeZone.js +43 -10
- package/dist/lib/codec/MapiTimeZone.js.map +1 -1
- package/dist/lib/codec/PropertyValue.js +28 -2
- package/dist/lib/codec/PropertyValue.js.map +1 -1
- package/dist/lib/codec/RopBuffer.js +15 -3
- package/dist/lib/codec/RopBuffer.js.map +1 -1
- package/dist/lib/mongo/MapiEmsmdbRouteMongo.js +2 -1
- package/dist/lib/mongo/MapiEmsmdbRouteMongo.js.map +1 -1
- package/dist/lib/nspi/NspiCodec.js +5 -5
- package/dist/lib/nspi/NspiCodec.js.map +1 -1
- package/dist/lib/nspi/NspiGetMatchesHandler.js +14 -14
- package/dist/lib/nspi/NspiGetMatchesHandler.js.map +1 -1
- package/dist/lib/rop/AddressList.js +109 -0
- package/dist/lib/rop/AddressList.js.map +1 -0
- package/dist/lib/rop/CalendarEventTarget.js +7 -4
- package/dist/lib/rop/CalendarEventTarget.js.map +1 -1
- package/dist/lib/rop/ContactTarget.js +2 -6
- package/dist/lib/rop/ContactTarget.js.map +1 -1
- package/dist/lib/rop/ContentsTable.js +60 -0
- package/dist/lib/rop/ContentsTable.js.map +1 -0
- package/dist/lib/rop/ExecuteBudget.js +82 -0
- package/dist/lib/rop/ExecuteBudget.js.map +1 -0
- package/dist/lib/rop/FastTransferStream.js +99 -2
- package/dist/lib/rop/FastTransferStream.js.map +1 -1
- package/dist/lib/rop/FolderTarget.js +13 -6
- package/dist/lib/rop/FolderTarget.js.map +1 -1
- package/dist/lib/rop/HandleDataCache.js +412 -0
- package/dist/lib/rop/HandleDataCache.js.map +1 -0
- package/dist/lib/rop/MeetingMessageClassHandler.js +194 -48
- package/dist/lib/rop/MeetingMessageClassHandler.js.map +1 -1
- package/dist/lib/rop/MessageBodyStream.js +33 -7
- package/dist/lib/rop/MessageBodyStream.js.map +1 -1
- package/dist/lib/rop/MessageTarget.js +17 -4
- package/dist/lib/rop/MessageTarget.js.map +1 -1
- package/dist/lib/rop/NamedPropertyRegistry.js +8 -5
- package/dist/lib/rop/NamedPropertyRegistry.js.map +1 -1
- package/dist/lib/rop/PropertyResolvers.js +3 -1
- package/dist/lib/rop/PropertyResolvers.js.map +1 -1
- package/dist/lib/rop/RepoPaging.js +54 -0
- package/dist/lib/rop/RepoPaging.js.map +1 -0
- package/dist/lib/rop/RestapiRules.js +52 -0
- package/dist/lib/rop/RestapiRules.js.map +1 -0
- package/dist/lib/rop/RopCreateMessageHandler.js +4 -2
- package/dist/lib/rop/RopCreateMessageHandler.js.map +1 -1
- package/dist/lib/rop/RopDeleteFolderHandler.js +118 -77
- package/dist/lib/rop/RopDeleteFolderHandler.js.map +1 -1
- package/dist/lib/rop/RopDeleteMessagesHandler.js +26 -1
- package/dist/lib/rop/RopDeleteMessagesHandler.js.map +1 -1
- package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js +14 -14
- package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js.map +1 -1
- package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js +13 -14
- package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js.map +1 -1
- package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js +33 -32
- package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js.map +1 -1
- package/dist/lib/rop/RopGetContentsTableHandler.js +17 -38
- package/dist/lib/rop/RopGetContentsTableHandler.js.map +1 -1
- package/dist/lib/rop/RopGetHierarchyTableHandler.js +4 -2
- package/dist/lib/rop/RopGetHierarchyTableHandler.js.map +1 -1
- package/dist/lib/rop/RopGetPropertiesSpecificHandler.js +6 -8
- package/dist/lib/rop/RopGetPropertiesSpecificHandler.js.map +1 -1
- package/dist/lib/rop/RopHandler.js +5 -1
- package/dist/lib/rop/RopHandler.js.map +1 -1
- package/dist/lib/rop/RopLogonHandler.js +5 -2
- package/dist/lib/rop/RopLogonHandler.js.map +1 -1
- package/dist/lib/rop/RopOpenFolderHandler.js +3 -1
- package/dist/lib/rop/RopOpenFolderHandler.js.map +1 -1
- package/dist/lib/rop/RopOpenMessageHandler.js +3 -1
- package/dist/lib/rop/RopOpenMessageHandler.js.map +1 -1
- package/dist/lib/rop/RopOpenStreamHandler.js +11 -8
- package/dist/lib/rop/RopOpenStreamHandler.js.map +1 -1
- package/dist/lib/rop/RopQueryRowsHandler.js +31 -5
- package/dist/lib/rop/RopQueryRowsHandler.js.map +1 -1
- package/dist/lib/rop/RopReadStreamHandler.js +22 -4
- package/dist/lib/rop/RopReadStreamHandler.js.map +1 -1
- package/dist/lib/rop/RopReleaseHandler.js +4 -1
- package/dist/lib/rop/RopReleaseHandler.js.map +1 -1
- package/dist/lib/rop/RopSaveChangesMessageHandler.js +66 -7
- package/dist/lib/rop/RopSaveChangesMessageHandler.js.map +1 -1
- package/dist/lib/rop/RopSetColumnsHandler.js +6 -8
- package/dist/lib/rop/RopSetColumnsHandler.js.map +1 -1
- package/dist/lib/rop/RopSetPropertiesHandler.js +23 -12
- package/dist/lib/rop/RopSetPropertiesHandler.js.map +1 -1
- package/dist/lib/rop/RopSubmitMessageHandler.js +148 -120
- package/dist/lib/rop/RopSubmitMessageHandler.js.map +1 -1
- package/dist/lib/rop/RopWriteStreamHandler.js +37 -10
- package/dist/lib/rop/RopWriteStreamHandler.js.map +1 -1
- package/dist/lib/rop/TaskTarget.js +2 -6
- package/dist/lib/rop/TaskTarget.js.map +1 -1
- package/dist/lib/rop/TransportSend.js +16 -0
- package/dist/lib/rop/TransportSend.js.map +1 -0
- package/dist/lib/sql/MapiEmsmdbRouteSQL.js +2 -1
- package/dist/lib/sql/MapiEmsmdbRouteSQL.js.map +1 -1
- package/dist/types/BaseMapiEmsmdbRoute.d.ts +59 -2
- package/dist/types/MapiSessionManager.d.ts +218 -63
- package/dist/types/RegexPatternUtils.d.ts +14 -0
- package/dist/types/RopDispatcher.d.ts +47 -11
- package/dist/types/codec/BufferCursor.d.ts +14 -0
- package/dist/types/codec/GlobalObjectId.d.ts +9 -5
- package/dist/types/codec/PropertyValue.d.ts +10 -0
- package/dist/types/codec/RopBuffer.d.ts +7 -0
- package/dist/types/mongo/MapiEmsmdbRouteMongo.d.ts +1 -0
- package/dist/types/nspi/NspiGetMatchesHandler.d.ts +2 -0
- package/dist/types/rop/AddressList.d.ts +48 -0
- package/dist/types/rop/CalendarEventTarget.d.ts +7 -3
- package/dist/types/rop/ContactTarget.d.ts +3 -3
- package/dist/types/rop/ContentsTable.d.ts +16 -0
- package/dist/types/rop/ExecuteBudget.d.ts +48 -0
- package/dist/types/rop/FastTransferStream.d.ts +41 -2
- package/dist/types/rop/FolderTarget.d.ts +3 -2
- package/dist/types/rop/HandleDataCache.d.ts +145 -0
- package/dist/types/rop/MeetingMessageClassHandler.d.ts +23 -29
- package/dist/types/rop/MessageBodyStream.d.ts +16 -7
- package/dist/types/rop/MessageTarget.d.ts +10 -3
- package/dist/types/rop/NamedPropertyRegistry.d.ts +6 -2
- package/dist/types/rop/PropertyResolvers.d.ts +1 -1
- package/dist/types/rop/RepoPaging.d.ts +30 -0
- package/dist/types/rop/RestapiRules.d.ts +32 -0
- package/dist/types/rop/RopCreateMessageHandler.d.ts +1 -0
- package/dist/types/rop/RopDeleteFolderHandler.d.ts +36 -21
- package/dist/types/rop/RopDeleteMessagesHandler.d.ts +7 -0
- package/dist/types/rop/RopFastTransferSourceCopyPropertiesHandler.d.ts +1 -0
- package/dist/types/rop/RopFastTransferSourceCopyToHandler.d.ts +1 -0
- package/dist/types/rop/RopFastTransferSourceGetBufferHandler.d.ts +10 -11
- package/dist/types/rop/RopGetContentsTableHandler.d.ts +7 -13
- package/dist/types/rop/RopGetHierarchyTableHandler.d.ts +1 -0
- package/dist/types/rop/RopHandler.d.ts +40 -1
- package/dist/types/rop/RopOpenFolderHandler.d.ts +1 -0
- package/dist/types/rop/RopOpenMessageHandler.d.ts +1 -0
- package/dist/types/rop/RopOpenStreamHandler.d.ts +1 -0
- package/dist/types/rop/RopQueryRowsHandler.d.ts +3 -0
- package/dist/types/rop/RopReadStreamHandler.d.ts +3 -1
- package/dist/types/rop/RopReleaseHandler.d.ts +1 -0
- package/dist/types/rop/RopSaveChangesMessageHandler.d.ts +16 -1
- package/dist/types/rop/RopSubmitMessageHandler.d.ts +33 -23
- package/dist/types/rop/RopWriteStreamHandler.d.ts +18 -8
- package/dist/types/rop/TaskTarget.d.ts +3 -3
- package/dist/types/rop/TransportSend.d.ts +10 -0
- package/dist/types/sql/MapiEmsmdbRouteSQL.d.ts +1 -0
- 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
|
|
5
|
-
* `RecoverableRepoUtils.delete()`
|
|
6
|
-
*
|
|
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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
* **
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
7
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
3
|
-
|
|
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).
|
|
26
|
-
* addresses
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* than
|
|
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
|
-
*
|
|
57
|
-
*
|
|
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
|
-
*
|
|
62
|
-
*
|
|
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
|
-
*
|
|
65
|
-
*
|
|
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 {
|
|
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`).
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
23
|
-
|
|
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>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rapidmx/mapi-plugin",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
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.
|
|
34
|
+
"@rapidmx/restapi": "^0.17.0",
|
|
35
35
|
"@rapidrest/core": "^5.2.2",
|
|
36
|
-
"@rapidrest/service-core": "^2.
|
|
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",
|