@rapidmx/mapi-plugin 1.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +382 -0
- package/README.md +60 -0
- package/dist/lib/BaseMapiEmsmdbRoute.js +489 -0
- package/dist/lib/BaseMapiEmsmdbRoute.js.map +1 -0
- package/dist/lib/BaseMapiNspiRoute.js +114 -0
- package/dist/lib/BaseMapiNspiRoute.js.map +1 -0
- package/dist/lib/MapiSessionManager.js +475 -0
- package/dist/lib/MapiSessionManager.js.map +1 -0
- package/dist/lib/RegexPatternUtils.js +30 -0
- package/dist/lib/RegexPatternUtils.js.map +1 -0
- package/dist/lib/RopDispatcher.js +145 -0
- package/dist/lib/RopDispatcher.js.map +1 -0
- package/dist/lib/codec/AppointmentRecurrence.js +315 -0
- package/dist/lib/codec/AppointmentRecurrence.js.map +1 -0
- package/dist/lib/codec/BufferCursor.js +242 -0
- package/dist/lib/codec/BufferCursor.js.map +1 -0
- package/dist/lib/codec/GlobalObjectId.js +98 -0
- package/dist/lib/codec/GlobalObjectId.js.map +1 -0
- package/dist/lib/codec/MapiGuid.js +41 -0
- package/dist/lib/codec/MapiGuid.js.map +1 -0
- package/dist/lib/codec/MapiTimeZone.js +126 -0
- package/dist/lib/codec/MapiTimeZone.js.map +1 -0
- package/dist/lib/codec/PropertyValue.js +240 -0
- package/dist/lib/codec/PropertyValue.js.map +1 -0
- package/dist/lib/codec/RopBuffer.js +38 -0
- package/dist/lib/codec/RopBuffer.js.map +1 -0
- package/dist/lib/codec/TypedString.js +54 -0
- package/dist/lib/codec/TypedString.js.map +1 -0
- package/dist/lib/index.js +99 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/mongo/MapiEmsmdbRouteMongo.js +85 -0
- package/dist/lib/mongo/MapiEmsmdbRouteMongo.js.map +1 -0
- package/dist/lib/mongo/MapiNspiRouteMongo.js +33 -0
- package/dist/lib/mongo/MapiNspiRouteMongo.js.map +1 -0
- package/dist/lib/mongo/index.js +7 -0
- package/dist/lib/mongo/index.js.map +1 -0
- package/dist/lib/mongo.js +6 -0
- package/dist/lib/mongo.js.map +1 -0
- package/dist/lib/nspi/NspiBindHandler.js +54 -0
- package/dist/lib/nspi/NspiBindHandler.js.map +1 -0
- package/dist/lib/nspi/NspiCodec.js +106 -0
- package/dist/lib/nspi/NspiCodec.js.map +1 -0
- package/dist/lib/nspi/NspiGetMatchesHandler.js +142 -0
- package/dist/lib/nspi/NspiGetMatchesHandler.js.map +1 -0
- package/dist/lib/rop/AddressList.js +114 -0
- package/dist/lib/rop/AddressList.js.map +1 -0
- package/dist/lib/rop/CalendarEventTarget.js +30 -0
- package/dist/lib/rop/CalendarEventTarget.js.map +1 -0
- package/dist/lib/rop/CalendarNamedProperties.js +57 -0
- package/dist/lib/rop/CalendarNamedProperties.js.map +1 -0
- package/dist/lib/rop/ContactTarget.js +19 -0
- package/dist/lib/rop/ContactTarget.js.map +1 -0
- 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 +195 -0
- package/dist/lib/rop/FastTransferStream.js.map +1 -0
- package/dist/lib/rop/FolderTarget.js +118 -0
- package/dist/lib/rop/FolderTarget.js.map +1 -0
- package/dist/lib/rop/HandleDataCache.js +412 -0
- package/dist/lib/rop/HandleDataCache.js.map +1 -0
- package/dist/lib/rop/MeetingMessageClassHandler.js +241 -0
- package/dist/lib/rop/MeetingMessageClassHandler.js.map +1 -0
- package/dist/lib/rop/MessageBodyStream.js +68 -0
- package/dist/lib/rop/MessageBodyStream.js.map +1 -0
- package/dist/lib/rop/MessageTarget.js +57 -0
- package/dist/lib/rop/MessageTarget.js.map +1 -0
- package/dist/lib/rop/NamedPropertyRegistry.js +69 -0
- package/dist/lib/rop/NamedPropertyRegistry.js.map +1 -0
- package/dist/lib/rop/PropertyResolvers.js +353 -0
- package/dist/lib/rop/PropertyResolvers.js.map +1 -0
- 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 +55 -0
- package/dist/lib/rop/RopCreateMessageHandler.js.map +1 -0
- package/dist/lib/rop/RopDeleteFolderHandler.js +174 -0
- package/dist/lib/rop/RopDeleteFolderHandler.js.map +1 -0
- package/dist/lib/rop/RopDeleteMessagesHandler.js +110 -0
- package/dist/lib/rop/RopDeleteMessagesHandler.js.map +1 -0
- package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js +61 -0
- package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js.map +1 -0
- package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js +60 -0
- package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js.map +1 -0
- package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js +76 -0
- package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js.map +1 -0
- package/dist/lib/rop/RopGetContentsTableHandler.js +55 -0
- package/dist/lib/rop/RopGetContentsTableHandler.js.map +1 -0
- package/dist/lib/rop/RopGetHierarchyTableHandler.js +42 -0
- package/dist/lib/rop/RopGetHierarchyTableHandler.js.map +1 -0
- package/dist/lib/rop/RopGetPropertiesSpecificHandler.js +56 -0
- package/dist/lib/rop/RopGetPropertiesSpecificHandler.js.map +1 -0
- package/dist/lib/rop/RopGetPropertyIdsFromNamesHandler.js +92 -0
- package/dist/lib/rop/RopGetPropertyIdsFromNamesHandler.js.map +1 -0
- package/dist/lib/rop/RopHandler.js +6 -0
- package/dist/lib/rop/RopHandler.js.map +1 -0
- package/dist/lib/rop/RopLogonHandler.js +135 -0
- package/dist/lib/rop/RopLogonHandler.js.map +1 -0
- package/dist/lib/rop/RopOpenFolderHandler.js +48 -0
- package/dist/lib/rop/RopOpenFolderHandler.js.map +1 -0
- package/dist/lib/rop/RopOpenMessageHandler.js +83 -0
- package/dist/lib/rop/RopOpenMessageHandler.js.map +1 -0
- package/dist/lib/rop/RopOpenStreamHandler.js +84 -0
- package/dist/lib/rop/RopOpenStreamHandler.js.map +1 -0
- package/dist/lib/rop/RopQueryRowsHandler.js +120 -0
- package/dist/lib/rop/RopQueryRowsHandler.js.map +1 -0
- package/dist/lib/rop/RopReadStreamHandler.js +73 -0
- package/dist/lib/rop/RopReadStreamHandler.js.map +1 -0
- package/dist/lib/rop/RopReleaseHandler.js +23 -0
- package/dist/lib/rop/RopReleaseHandler.js.map +1 -0
- package/dist/lib/rop/RopSaveChangesMessageHandler.js +269 -0
- package/dist/lib/rop/RopSaveChangesMessageHandler.js.map +1 -0
- package/dist/lib/rop/RopSetColumnsHandler.js +50 -0
- package/dist/lib/rop/RopSetColumnsHandler.js.map +1 -0
- package/dist/lib/rop/RopSetPropertiesHandler.js +173 -0
- package/dist/lib/rop/RopSetPropertiesHandler.js.map +1 -0
- package/dist/lib/rop/RopSubmitMessageHandler.js +342 -0
- package/dist/lib/rop/RopSubmitMessageHandler.js.map +1 -0
- package/dist/lib/rop/RopWriteStreamHandler.js +72 -0
- package/dist/lib/rop/RopWriteStreamHandler.js.map +1 -0
- package/dist/lib/rop/TaskNamedProperties.js +29 -0
- package/dist/lib/rop/TaskNamedProperties.js.map +1 -0
- package/dist/lib/rop/TaskTarget.js +15 -0
- package/dist/lib/rop/TaskTarget.js.map +1 -0
- package/dist/lib/rop/TransportSend.js +16 -0
- package/dist/lib/rop/TransportSend.js.map +1 -0
- package/dist/lib/sql/MapiEmsmdbRouteSQL.js +85 -0
- package/dist/lib/sql/MapiEmsmdbRouteSQL.js.map +1 -0
- package/dist/lib/sql/MapiNspiRouteSQL.js +33 -0
- package/dist/lib/sql/MapiNspiRouteSQL.js.map +1 -0
- package/dist/lib/sql/index.js +7 -0
- package/dist/lib/sql/index.js.map +1 -0
- package/dist/lib/sql.js +6 -0
- package/dist/lib/sql.js.map +1 -0
- package/dist/types/BaseMapiEmsmdbRoute.d.ts +139 -0
- package/dist/types/BaseMapiNspiRoute.d.ts +38 -0
- package/dist/types/MapiSessionManager.d.ts +285 -0
- package/dist/types/RegexPatternUtils.d.ts +14 -0
- package/dist/types/RopDispatcher.d.ts +53 -0
- package/dist/types/codec/AppointmentRecurrence.d.ts +14 -0
- package/dist/types/codec/BufferCursor.d.ts +92 -0
- package/dist/types/codec/GlobalObjectId.d.ts +15 -0
- package/dist/types/codec/MapiGuid.d.ts +6 -0
- package/dist/types/codec/MapiTimeZone.d.ts +7 -0
- package/dist/types/codec/PropertyValue.d.ts +87 -0
- package/dist/types/codec/RopBuffer.d.ts +32 -0
- package/dist/types/codec/TypedString.d.ts +17 -0
- package/dist/types/index.d.ts +94 -0
- package/dist/types/mongo/MapiEmsmdbRouteMongo.d.ts +20 -0
- package/dist/types/mongo/MapiNspiRouteMongo.d.ts +13 -0
- package/dist/types/mongo/index.d.ts +2 -0
- package/dist/types/mongo.d.ts +1 -0
- package/dist/types/nspi/NspiBindHandler.d.ts +22 -0
- package/dist/types/nspi/NspiCodec.d.ts +59 -0
- package/dist/types/nspi/NspiGetMatchesHandler.d.ts +26 -0
- package/dist/types/rop/AddressList.d.ts +48 -0
- package/dist/types/rop/CalendarEventTarget.d.ts +33 -0
- package/dist/types/rop/CalendarNamedProperties.d.ts +37 -0
- package/dist/types/rop/ContactTarget.d.ts +29 -0
- 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 +57 -0
- package/dist/types/rop/FolderTarget.d.ts +64 -0
- package/dist/types/rop/HandleDataCache.d.ts +145 -0
- package/dist/types/rop/MeetingMessageClassHandler.d.ts +30 -0
- package/dist/types/rop/MessageBodyStream.d.ts +35 -0
- package/dist/types/rop/MessageTarget.d.ts +45 -0
- package/dist/types/rop/NamedPropertyRegistry.d.ts +50 -0
- package/dist/types/rop/PropertyResolvers.d.ts +86 -0
- 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 +24 -0
- package/dist/types/rop/RopDeleteFolderHandler.d.ts +53 -0
- package/dist/types/rop/RopDeleteMessagesHandler.d.ts +43 -0
- package/dist/types/rop/RopFastTransferSourceCopyPropertiesHandler.d.ts +25 -0
- package/dist/types/rop/RopFastTransferSourceCopyToHandler.d.ts +25 -0
- package/dist/types/rop/RopFastTransferSourceGetBufferHandler.d.ts +22 -0
- package/dist/types/rop/RopGetContentsTableHandler.d.ts +25 -0
- package/dist/types/rop/RopGetHierarchyTableHandler.d.ts +18 -0
- package/dist/types/rop/RopGetPropertiesSpecificHandler.d.ts +26 -0
- package/dist/types/rop/RopGetPropertyIdsFromNamesHandler.d.ts +23 -0
- package/dist/types/rop/RopHandler.d.ts +106 -0
- package/dist/types/rop/RopLogonHandler.d.ts +31 -0
- package/dist/types/rop/RopOpenFolderHandler.d.ts +20 -0
- package/dist/types/rop/RopOpenMessageHandler.d.ts +28 -0
- package/dist/types/rop/RopOpenStreamHandler.d.ts +28 -0
- package/dist/types/rop/RopQueryRowsHandler.d.ts +43 -0
- package/dist/types/rop/RopReadStreamHandler.d.ts +20 -0
- package/dist/types/rop/RopReleaseHandler.d.ts +15 -0
- package/dist/types/rop/RopSaveChangesMessageHandler.d.ts +59 -0
- package/dist/types/rop/RopSetColumnsHandler.d.ts +21 -0
- package/dist/types/rop/RopSetPropertiesHandler.d.ts +38 -0
- package/dist/types/rop/RopSubmitMessageHandler.d.ts +80 -0
- package/dist/types/rop/RopWriteStreamHandler.d.ts +31 -0
- package/dist/types/rop/TaskNamedProperties.d.ts +24 -0
- package/dist/types/rop/TaskTarget.d.ts +24 -0
- package/dist/types/rop/TransportSend.d.ts +10 -0
- package/dist/types/sql/MapiEmsmdbRouteSQL.d.ts +20 -0
- package/dist/types/sql/MapiNspiRouteSQL.d.ts +13 -0
- package/dist/types/sql/index.d.ts +2 -0
- package/dist/types/sql.d.ts +1 -0
- package/package.json +104 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** How long an entry lives without being read - the same idle lifetime as a session. */
|
|
2
|
+
export declare const HANDLE_DATA_TTL_MS: number;
|
|
3
|
+
/** The most bytes this process keeps across all entries before evicting the least recently used ones. */
|
|
4
|
+
export declare const HANDLE_DATA_MAX_BYTES: number;
|
|
5
|
+
/**
|
|
6
|
+
* A process-local, byte-bounded LRU with TTL for large per-handle data: an opened stream's decoded message body
|
|
7
|
+
* and a FastTransfer handle's built stream. Keeping these out of the session JSON means an `Execute` doesn't
|
|
8
|
+
* re-serialize megabytes to Redis, and a `RopReadStream` doesn't re-fetch and re-parse the whole MIME message
|
|
9
|
+
* for every chunk.
|
|
10
|
+
*
|
|
11
|
+
* It is only a cache. Every caller can rebuild an entry from the handle's own (small) session state, so a miss
|
|
12
|
+
* (another replica served the previous request, eviction, TTL) costs one rebuild, never wrong data. Keys carry
|
|
13
|
+
* the handle's generation (see `assignHandle`), so a reused handle index never picks up a previous handle's data.
|
|
14
|
+
*/
|
|
15
|
+
export declare class HandleDataCache {
|
|
16
|
+
private readonly maxBytes;
|
|
17
|
+
private readonly ttlMs;
|
|
18
|
+
private readonly entries;
|
|
19
|
+
private totalBytes;
|
|
20
|
+
constructor(maxBytes?: number, ttlMs?: number);
|
|
21
|
+
get(key: string): Buffer | undefined;
|
|
22
|
+
/** Stores `value` under `key`. A value larger than the whole budget isn't cached at all. */
|
|
23
|
+
set(key: string, value: Buffer): void;
|
|
24
|
+
/** The size of the live entry at `key`, without refreshing it or changing its recency. */
|
|
25
|
+
sizeOf(key: string): number | undefined;
|
|
26
|
+
delete(key: string): void;
|
|
27
|
+
/** The bytes currently held, for tests and diagnostics. */
|
|
28
|
+
get size(): number;
|
|
29
|
+
}
|
|
30
|
+
/** The process-wide instance every handler shares. */
|
|
31
|
+
export declare const handleDataCache: HandleDataCache;
|
|
32
|
+
/** The cache key for the handle at `handleIndex`, as of its current generation. A generation is a random nonce
|
|
33
|
+
* (see `assignHandle`), so a key is never reused, not even by a request whose session changes were never saved. */
|
|
34
|
+
export declare function handleDataKey(sessionUid: string, handleIndex: number, generation: string | undefined): string;
|
|
35
|
+
/** The `HandleDataStore` key of the write stream at `handleIndex`. */
|
|
36
|
+
export declare function writeStreamKey(sessionUid: string, handleIndex: number, generation: string | undefined): string;
|
|
37
|
+
/** The most bytes of FastTransfer streams and write-stream chunks one session keeps stored at once. */
|
|
38
|
+
export declare const MAX_HANDLE_DATA_BYTES_PER_SESSION: number;
|
|
39
|
+
/** The most bytes of FastTransfer streams and write-stream chunks one user keeps stored at once, across all of their
|
|
40
|
+
* sessions. */
|
|
41
|
+
export declare const MAX_HANDLE_DATA_BYTES_PER_USER: number;
|
|
42
|
+
/** How long an owner index outlives its last write: a session's longest lifetime. */
|
|
43
|
+
export declare const HANDLE_DATA_INDEX_TTL_SECONDS: number;
|
|
44
|
+
/** The most chunks one write stream keeps. Each `RopWriteStream` stores one chunk, so this bounds the hash a stream
|
|
45
|
+
* builds up (and the work `readChunks` does) however small the writes are. */
|
|
46
|
+
export declare const MAX_CHUNKS_PER_STREAM = 8192;
|
|
47
|
+
/** How often, at most, an owner index that is over its quota is recounted from its members. Between recounts a write is
|
|
48
|
+
* checked against the index's running total alone, so a client hammering a full quota can't make every write walk
|
|
49
|
+
* every member. */
|
|
50
|
+
export declare const OWNER_RECOUNT_INTERVAL_MS = 1000;
|
|
51
|
+
/** An index that stored data is counted against: the keys it holds and the most bytes they may add up to. */
|
|
52
|
+
export interface HandleDataOwner {
|
|
53
|
+
index: string;
|
|
54
|
+
maxBytes: number;
|
|
55
|
+
}
|
|
56
|
+
/** The index of everything a session stores, used for its quota and to delete it all when the session ends. */
|
|
57
|
+
export declare function sessionHandleDataIndex(sessionUid: string): string;
|
|
58
|
+
/** The quota owners a write by `sessionUid` (belonging to `userUid`) counts against. */
|
|
59
|
+
export declare function handleDataOwners(sessionUid: string, userUid: string): HandleDataOwner[];
|
|
60
|
+
/**
|
|
61
|
+
* Where per-handle data that has to outlive one `Execute` lives: a built FastTransfer stream, and the chunks a write
|
|
62
|
+
* stream has accumulated. With Redis (`RedisHandleDataStore`) every replica sees the same data, so a client can page
|
|
63
|
+
* a stream out or finish writing a body through any pod. Without it (`MemoryHandleDataStore`) the data is only in
|
|
64
|
+
* this process.
|
|
65
|
+
*
|
|
66
|
+
* **Quotas.** A write given `owners` is recorded in each owner's index and refused (`false`, nothing stored) when it
|
|
67
|
+
* would take any owner past its `maxBytes`. Sizes are raw (unencoded) bytes. Every write costs O(1) per owner: each
|
|
68
|
+
* index keeps a running total, and a chunked stream keeps its own size (the end of its furthest chunk), so neither the
|
|
69
|
+
* stream's chunks nor the index's members are walked. A delete given the same `owners` takes the key's bytes off their
|
|
70
|
+
* totals. Data that disappears otherwise (TTL, eviction, `deleteOwnedBy` of another index, a delete without owners)
|
|
71
|
+
* keeps counting until a write that the total would refuse recounts the index from its live members, at most once per
|
|
72
|
+
* `OWNER_RECOUNT_INTERVAL_MS` per index. Totals only ever over-count, never under-count.
|
|
73
|
+
*/
|
|
74
|
+
export interface HandleDataStore {
|
|
75
|
+
get(key: string): Promise<Buffer | undefined>;
|
|
76
|
+
/** Stores `value` at `key`. `false` when an owner's quota refused it. */
|
|
77
|
+
set(key: string, value: Buffer, ttlSeconds: number, owners?: HandleDataOwner[]): Promise<boolean>;
|
|
78
|
+
/** Stores `data` as the chunk starting at byte `offset` of the stream at `key`. Writing the same offset again
|
|
79
|
+
* replaces the chunk, so a retried write never duplicates bytes or charges the quota twice. `false` when an owner's
|
|
80
|
+
* quota refused it, or the stream already has `MAX_CHUNKS_PER_STREAM` chunks and this is a new one. */
|
|
81
|
+
putChunk(key: string, offset: number, data: Buffer, ttlSeconds: number, owners?: HandleDataOwner[]): Promise<boolean>;
|
|
82
|
+
/** The first `size` bytes of the stream at `key`, assembled from its chunks, or `undefined` when a chunk is
|
|
83
|
+
* missing (expired or evicted) and the stream can't be reassembled exactly. */
|
|
84
|
+
readChunks(key: string, size: number): Promise<Buffer | undefined>;
|
|
85
|
+
/** Deletes the value or chunked stream at `key`, taking it off the totals of `owners`. */
|
|
86
|
+
delete(key: string, owners?: HandleDataOwner[]): Promise<void>;
|
|
87
|
+
/** Deletes everything recorded in the owner index `index`, and the index itself. */
|
|
88
|
+
deleteOwnedBy(index: string): Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
/** A `HandleDataStore` in this process only, on a `HandleDataCache` (so it is byte-bounded and can evict). */
|
|
91
|
+
export declare class MemoryHandleDataStore implements HandleDataStore {
|
|
92
|
+
private readonly cache;
|
|
93
|
+
private readonly indexes;
|
|
94
|
+
private readonly streams;
|
|
95
|
+
constructor(cache?: HandleDataCache);
|
|
96
|
+
get(key: string): Promise<Buffer | undefined>;
|
|
97
|
+
set(key: string, value: Buffer, _ttlSeconds: number, owners?: HandleDataOwner[]): Promise<boolean>;
|
|
98
|
+
putChunk(key: string, offset: number, data: Buffer, _ttlSeconds: number, owners?: HandleDataOwner[]): Promise<boolean>;
|
|
99
|
+
readChunks(key: string, size: number): Promise<Buffer | undefined>;
|
|
100
|
+
delete(key: string, owners?: HandleDataOwner[]): Promise<void>;
|
|
101
|
+
deleteOwnedBy(index: string): Promise<void>;
|
|
102
|
+
/** `true` while `key` still holds a value or its stream's first chunk (a stream missing that can't be read). */
|
|
103
|
+
private exists;
|
|
104
|
+
/** Records `key` at `bytes` in every owner index, unless that would take one past its quota. */
|
|
105
|
+
private admit;
|
|
106
|
+
/** `true` when `key` at `bytes` keeps `index` within `maxBytes`, recounting an index that looks full (rate limited). */
|
|
107
|
+
private fits;
|
|
108
|
+
}
|
|
109
|
+
/** Stores a value within its owners' quotas. KEYS: data key, then each owner index. ARGV: data key prefix, member name,
|
|
110
|
+
* index ttl, recount interval ms, base64 value, ttl, raw bytes, then each owner's max bytes. Returns 1 stored, 0 refused. */
|
|
111
|
+
export declare const SET_WITH_QUOTA_SCRIPT = "\nlocal function recount(index, prefix, indexTtl)\n local total = 0\n local members = redis.call('ZRANGE', index, 0, -1, 'WITHSCORES')\n for j = 1, #members, 2 do\n if redis.call('EXISTS', prefix .. members[j]) == 0 then\n redis.call('ZREM', index, members[j])\n else\n total = total + tonumber(members[j + 1])\n end\n end\n redis.call('SET', index .. '.total', tostring(total), 'EX', indexTtl)\n return total\nend\nlocal function admits(index, member, bytes, maxBytes, prefix, recountMs, indexTtl)\n local stored = redis.call('GET', index .. '.total')\n local fresh = not stored\n local total = stored and tonumber(stored) or recount(index, prefix, indexTtl)\n local old = tonumber(redis.call('ZSCORE', index, member) or 0)\n if total - old + bytes <= maxBytes then return true end\n if fresh or not redis.call('SET', index .. '.recount', '1', 'PX', recountMs, 'NX') then return false end\n total = recount(index, prefix, indexTtl)\n old = tonumber(redis.call('ZSCORE', index, member) or 0)\n return total - old + bytes <= maxBytes\nend\nlocal function record(index, member, bytes, indexTtl)\n local old = tonumber(redis.call('ZSCORE', index, member) or 0)\n redis.call('ZADD', index, tostring(bytes), member)\n redis.call('INCRBY', index .. '.total', tostring(bytes - old))\n redis.call('EXPIRE', index, indexTtl)\n redis.call('EXPIRE', index .. '.total', indexTtl)\nend\n\nlocal bytes = tonumber(ARGV[7])\nfor i = 2, #KEYS do\n if not admits(KEYS[i], ARGV[2], bytes, tonumber(ARGV[i + 6]), ARGV[1], ARGV[4], ARGV[3]) then return 0 end\nend\nredis.call('SETEX', KEYS[1], ARGV[6], ARGV[5])\nfor i = 2, #KEYS do record(KEYS[i], ARGV[2], bytes, ARGV[3]) end\nreturn 1\n";
|
|
112
|
+
/** Stores one write-stream chunk within its owners' quotas, in O(1): the hash keeps its size (the end of its furthest
|
|
113
|
+
* chunk) in the `size` field, and that size is what the owners are charged, so a chunk rewritten at the same offset
|
|
114
|
+
* isn't charged twice. KEYS: hash key, then each owner index. ARGV: data key prefix, member name, index ttl, recount
|
|
115
|
+
* interval ms, unpadded base64url chunk, ttl, raw chunk bytes, hash field (offset), max chunks, then each owner's max
|
|
116
|
+
* bytes. Returns 1 stored, 0 refused (quota, or a new chunk past the max chunks). */
|
|
117
|
+
export declare const PUT_CHUNK_WITH_QUOTA_SCRIPT = "\nlocal function recount(index, prefix, indexTtl)\n local total = 0\n local members = redis.call('ZRANGE', index, 0, -1, 'WITHSCORES')\n for j = 1, #members, 2 do\n if redis.call('EXISTS', prefix .. members[j]) == 0 then\n redis.call('ZREM', index, members[j])\n else\n total = total + tonumber(members[j + 1])\n end\n end\n redis.call('SET', index .. '.total', tostring(total), 'EX', indexTtl)\n return total\nend\nlocal function admits(index, member, bytes, maxBytes, prefix, recountMs, indexTtl)\n local stored = redis.call('GET', index .. '.total')\n local fresh = not stored\n local total = stored and tonumber(stored) or recount(index, prefix, indexTtl)\n local old = tonumber(redis.call('ZSCORE', index, member) or 0)\n if total - old + bytes <= maxBytes then return true end\n if fresh or not redis.call('SET', index .. '.recount', '1', 'PX', recountMs, 'NX') then return false end\n total = recount(index, prefix, indexTtl)\n old = tonumber(redis.call('ZSCORE', index, member) or 0)\n return total - old + bytes <= maxBytes\nend\nlocal function record(index, member, bytes, indexTtl)\n local old = tonumber(redis.call('ZSCORE', index, member) or 0)\n redis.call('ZADD', index, tostring(bytes), member)\n redis.call('INCRBY', index .. '.total', tostring(bytes - old))\n redis.call('EXPIRE', index, indexTtl)\n redis.call('EXPIRE', index .. '.total', indexTtl)\nend\n\nlocal field = ARGV[8]\nif redis.call('HEXISTS', KEYS[1], field) == 0 and redis.call('HLEN', KEYS[1]) > tonumber(ARGV[9]) then return 0 end\nlocal size = math.max(tonumber(redis.call('HGET', KEYS[1], 'size') or 0), tonumber(field) + tonumber(ARGV[7]))\nfor i = 2, #KEYS do\n if not admits(KEYS[i], ARGV[2], size, tonumber(ARGV[i + 8]), ARGV[1], ARGV[4], ARGV[3]) then return 0 end\nend\nredis.call('HSET', KEYS[1], field, ARGV[5], 'size', tostring(size))\nredis.call('EXPIRE', KEYS[1], ARGV[6])\nfor i = 2, #KEYS do record(KEYS[i], ARGV[2], size, ARGV[3]) end\nreturn 1\n";
|
|
118
|
+
/** Deletes one data key and takes it off its owners' totals. KEYS: data key, then each owner index. ARGV: member name. */
|
|
119
|
+
export declare const DELETE_WITH_OWNERS_SCRIPT = "\nredis.call('DEL', KEYS[1])\nfor i = 2, #KEYS do\n local old = redis.call('ZSCORE', KEYS[i], ARGV[1])\n if old then\n redis.call('ZREM', KEYS[i], ARGV[1])\n if redis.call('EXISTS', KEYS[i] .. '.total') == 1 then redis.call('INCRBY', KEYS[i] .. '.total', tostring(-tonumber(old))) end\n end\nend\nreturn 1\n";
|
|
120
|
+
/** Deletes every key an owner index names, then the index with its total and recount marker. KEYS: index. ARGV: data key
|
|
121
|
+
* prefix. Returns the members (unprefixed keys) it deleted, so the caller can drop its local copies too. */
|
|
122
|
+
export declare const DELETE_OWNED_SCRIPT = "\nlocal members = redis.call('ZRANGE', KEYS[1], 0, -1)\nfor _, member in ipairs(members) do\n redis.call('DEL', ARGV[1] .. member)\nend\nredis.call('DEL', KEYS[1], KEYS[1] .. '.total', KEYS[1] .. '.recount')\nreturn members\n";
|
|
123
|
+
/** The prefix of every handle data key in Redis. */
|
|
124
|
+
export declare const HANDLE_DATA_PREFIX = "mapi.handle.";
|
|
125
|
+
/**
|
|
126
|
+
* A `HandleDataStore` on a node-redis client, with this process's `HandleDataCache` in front for reads. Values are
|
|
127
|
+
* stored base64-encoded (a plain string works with any client configuration); a write stream is a hash of
|
|
128
|
+
* `offset -> chunk` plus its `size`. Owner indexes are sorted sets (`mapi.handle-owner.<index>`) of key -> raw bytes, with
|
|
129
|
+
* a running total beside each (see `OWNER_INDEX_LUA`).
|
|
130
|
+
*/
|
|
131
|
+
export declare class RedisHandleDataStore implements HandleDataStore {
|
|
132
|
+
private readonly client;
|
|
133
|
+
private readonly local;
|
|
134
|
+
constructor(client: any, local?: HandleDataCache);
|
|
135
|
+
get(key: string): Promise<Buffer | undefined>;
|
|
136
|
+
set(key: string, value: Buffer, ttlSeconds: number, owners?: HandleDataOwner[]): Promise<boolean>;
|
|
137
|
+
putChunk(key: string, offset: number, data: Buffer, ttlSeconds: number, owners?: HandleDataOwner[]): Promise<boolean>;
|
|
138
|
+
readChunks(key: string, size: number): Promise<Buffer | undefined>;
|
|
139
|
+
delete(key: string, owners?: HandleDataOwner[]): Promise<void>;
|
|
140
|
+
deleteOwnedBy(index: string): Promise<void>;
|
|
141
|
+
}
|
|
142
|
+
/** The Redis key of an owner index. */
|
|
143
|
+
export declare function ownerKey(index: string): string;
|
|
144
|
+
/** The store used when a `RopContext` doesn't carry one (unit tests, or a route built without a session manager). */
|
|
145
|
+
export declare const defaultHandleDataStore: HandleDataStore;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RopContext } from "./RopHandler.js";
|
|
2
|
+
/**
|
|
3
|
+
* Handles a submitted `"IPM.Schedule.Meeting.Resp.{Pos,Neg,Tent}"` message - an attendee's own response to a
|
|
4
|
+
* meeting invite. Delegated to from `RopSubmitMessageHandler` for that message-class prefix, instead of the ordinary
|
|
5
|
+
* mail or Appointment paths. Returns the ROP's `ReturnValue`.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors restapi's `BaseCalendarEventRoute.respond()`. It correlates the response via `PidLidGlobalObjectId`
|
|
8
|
+
* (echoed by a real client from the invite it's responding to - see `GlobalObjectId.ts`) to **the caller's own
|
|
9
|
+
* copy** of the meeting: the query is scoped to the caller's `mailboxUid`, which is the access check. The
|
|
10
|
+
* organizer's copy (in another mailbox) is never touched here; the organizer's server applies the iTIP `REPLY` sent
|
|
11
|
+
* below, exactly as for a REST response. A native Outlook `GlobalObjectId` decodes to the uppercase hex form of its
|
|
12
|
+
* iCalendar UID, which is matched in either case.
|
|
13
|
+
*
|
|
14
|
+
* **One occurrence of a recurring meeting** (the `GlobalObjectId`'s instance date is set): the caller's exception
|
|
15
|
+
* copy for that date is used when there is one, comparing dates in the event's own time zone. Without one, the
|
|
16
|
+
* series is left alone - responding to one occurrence must not change or delete every other occurrence. Declining
|
|
17
|
+
* adds the occurrence to the caller's series `recurrenceRule.exceptions` (so it leaves their calendar); accepting or
|
|
18
|
+
* tentatively accepting changes nothing locally. Either way the `REPLY` names just that occurrence (`RECURRENCE-ID`).
|
|
19
|
+
*
|
|
20
|
+
* **The whole meeting** (no instance date, or a copy that doesn't recur): the caller's `Attendee.responseStatus` is
|
|
21
|
+
* recorded; declining soft-deletes the caller's copy instead. Declining an occurrence through its exception copy also
|
|
22
|
+
* adds that occurrence to the series' exceptions, so the series doesn't show it again. Every update is versioned
|
|
23
|
+
* (`asEntity`), so a concurrent change fails the response instead of being overwritten.
|
|
24
|
+
*
|
|
25
|
+
* The `REPLY` (restapi's own `buildEventIcs`) goes out through `sendOrThrow`, so a transport that rejects it is
|
|
26
|
+
* reported as `MAPI_E_CALL_FAILED` instead of success; the response itself stays recorded. An organizer or attendee
|
|
27
|
+
* address that isn't a plain SMTP address is not mailed at all. A response that can't be matched to a meeting the
|
|
28
|
+
* caller attends is an error (`MAPI_E_NOT_FOUND`/`MAPI_E_INVALID_PARAMETER`), not a silent success.
|
|
29
|
+
*/
|
|
30
|
+
export declare function submitMeetingResponse(messageClass: string, draftProperties: Record<string, string>, context: RopContext): Promise<number>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { RepoUtils } from "@rapidrest/service-core";
|
|
2
|
+
import { type BlobStore } from "@rapidmx/restapi";
|
|
3
|
+
import type { MapiObjectHandle } from "../MapiSessionManager.js";
|
|
4
|
+
import type { ExecuteBudget } from "./ExecuteBudget.js";
|
|
5
|
+
import type { RopContext } from "./RopHandler.js";
|
|
6
|
+
/** `PidTagBody` (`[MS-OXPROPS]`): property ID `0x1000`, `PtypString`. The only streamable property this
|
|
7
|
+
* pragmatic subset supports via `RopOpenStream`/`RopReadStream` - `PidTagHtml`/`PidTagRtfCompressed` are a
|
|
8
|
+
* documented gap (see `RopOpenStreamHandler`'s own doc comment). */
|
|
9
|
+
export declare const PID_TAG_BODY = 4096;
|
|
10
|
+
/**
|
|
11
|
+
* Resolves a `"message:<uid>"` target's plain-text body as the exact byte sequence `RopReadStream` must serve
|
|
12
|
+
* for a streamed `PtypString` property - confirmed via `[MS-OXCPRPT]`'s `RopOpenStream` page ("a string of
|
|
13
|
+
* Unicode characters in UTF-16LE format encoding with terminating null character"), the identical encoding
|
|
14
|
+
* `writePropertyValue()` already uses for an inline `PtypString` value, so this reuses
|
|
15
|
+
* `BufferWriter.writeNullTerminatedUtf16LE` rather than reimplementing it.
|
|
16
|
+
*
|
|
17
|
+
* Always derived from the message's raw MIME source (`bodyBlobKey`, via `mailparser` - the same fallback path
|
|
18
|
+
* `ItemOperationsCommand.fetchMessage()` already uses for EAS's own plain-text body delivery) rather than
|
|
19
|
+
* `sanitizedHtmlBlobKey` - `PidTagBody` is specifically the plain-text body per `[MS-OXPROPS]`, unlike EAS's
|
|
20
|
+
* `Body` element, which can carry either format tagged by its own `Type` field.
|
|
21
|
+
*
|
|
22
|
+
* Does no caching itself - `loadStreamBody` below is what the stream ROPs call. With a `budget`, nothing is fetched
|
|
23
|
+
* once its bytes are used up, and the raw message's size is charged before it is parsed.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveMessageBodyBytes(target: string, messageRepo: RepoUtils<any>, blobStore: BlobStore, budget?: ExecuteBudget): Promise<Buffer>;
|
|
26
|
+
/**
|
|
27
|
+
* The body bytes for the read stream at `handleIndex`. Parsed once and kept in `HandleDataCache` (outside the
|
|
28
|
+
* session JSON) for the life of the handle, so reading a large body in many `RopReadStream` chunks costs one
|
|
29
|
+
* fetch and one MIME parse instead of one per chunk. A cache miss (eviction, or a request served by another
|
|
30
|
+
* replica) just resolves the body again: a stored message's body never changes, so the bytes are the same.
|
|
31
|
+
*
|
|
32
|
+
* Within one `Execute`, streams opened on the same message share a single parse (`ExecuteBudget.bodies`), and each
|
|
33
|
+
* parse's bytes count against the request's byte budget.
|
|
34
|
+
*/
|
|
35
|
+
export declare function loadStreamBody(context: RopContext, handleIndex: number, stream: MapiObjectHandle): Promise<Buffer>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { RepoUtils } from "@rapidrest/service-core";
|
|
2
|
+
import type { MapiSessionContext } from "../MapiSessionManager.js";
|
|
3
|
+
import type { ExecuteBudget } from "./ExecuteBudget.js";
|
|
4
|
+
import { type RepoSort } from "./RepoPaging.js";
|
|
5
|
+
/**
|
|
6
|
+
* The `RopGetContentsTable` analog of `FolderTarget.ts`: resolves a `"message:<uid>"` row target (see
|
|
7
|
+
* `RopGetContentsTableHandler`'s own doc comment for why contents-table rows use this prefix instead of
|
|
8
|
+
* `FolderTarget.ts`'s `"folder:"`/`"virtual:"`) into the display data a message-table row needs.
|
|
9
|
+
*/
|
|
10
|
+
export interface MessageTargetInfo {
|
|
11
|
+
subject: string;
|
|
12
|
+
read: boolean;
|
|
13
|
+
hasAttachments: boolean;
|
|
14
|
+
receivedDate: Date;
|
|
15
|
+
/** `Label.uid`s applied to this message (Gmail-style, independent of folder placement) - resolved to
|
|
16
|
+
* display names for `PidNameKeywords` (Outlook Categories) by `PropertyResolvers.ts`, which is where the
|
|
17
|
+
* `labelRepo` needed to translate a uid into a name lives. */
|
|
18
|
+
labelUids: string[];
|
|
19
|
+
}
|
|
20
|
+
/** Degrades to empty-looking values for a `"message:<uid>"` target whose real `Message` has since vanished
|
|
21
|
+
* (soft-deleted or otherwise), the same "don't fail the whole ROP over one stale row" principle
|
|
22
|
+
* `FolderTarget.resolveFolderInfo` already applies. */
|
|
23
|
+
export declare function resolveMessageInfo(target: string, messageRepo: RepoUtils<any>): Promise<MessageTargetInfo>;
|
|
24
|
+
/** The order a folder's messages are listed in: newest first, ties broken by `uid` so paging is stable. */
|
|
25
|
+
export declare const MESSAGE_SORT: RepoSort;
|
|
26
|
+
/** Resolves the messages directly in `folderUid`, newest first and capped at `MAX_COLLECTION_ROWS`, as
|
|
27
|
+
* `"message:<uid>"` target strings. */
|
|
28
|
+
export declare function resolveFolderMessages(folderUid: string, messageRepo: RepoUtils<any>, budget?: ExecuteBudget): Promise<string[]>;
|
|
29
|
+
/**
|
|
30
|
+
* Returns `target`'s existing MID if an earlier `RopQueryRows` row already assigned one, otherwise assigns and
|
|
31
|
+
* remembers the next free small integer MID. The exact `FolderTarget.assignOrGetFid` pattern, adapted for
|
|
32
|
+
* messages: this is what lets a client `RopOpenMessage` a message it only ever learned about via a
|
|
33
|
+
* `RopQueryRows` row's `PidTagMid` column.
|
|
34
|
+
*
|
|
35
|
+
* Backed by `session.messageTargetIds` (target -> MID) and `session.nextMessageId`, an O(1) reverse index/
|
|
36
|
+
* counter pair rather than a linear scan of `session.messageIds` plus a `Math.max(...spread)` over its keys -
|
|
37
|
+
* see `FolderTarget.assignOrGetFid`'s own doc comment for why both of those were real costs (not just
|
|
38
|
+
* theoretical ones) at real mailbox/session scale: `messageIds` only ever grows for a session's lifetime as a
|
|
39
|
+
* client pages through a mailbox, so a linear-scan lookup repeated once per row is quadratic over a session
|
|
40
|
+
* that pages through many messages.
|
|
41
|
+
*/
|
|
42
|
+
export declare function assignOrGetMid(session: MapiSessionContext, target: string): number;
|
|
43
|
+
/** The most MIDs one session keeps mapped. A session paging through a very large mailbox would otherwise grow its
|
|
44
|
+
* MID maps - saved with the session on every `Execute` - without bound. */
|
|
45
|
+
export declare const MAX_MESSAGE_IDS = 20000;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { MapiSessionContext } from "../MapiSessionManager.js";
|
|
2
|
+
/** A `PropertyName` (`[MS-OXCDATA]` §2.6.1): identifies a named property by a property-set `guid` plus either a
|
|
3
|
+
* numeric `lid` (`kind: "lid"`) or a string `name` (`kind: "name"`) - never both. `guid` is the textual form
|
|
4
|
+
* `MapiGuid.ts`'s `encodeGuid`/`decodeGuid` already produce/consume (the wire `PropertyName` structure's own
|
|
5
|
+
* `GUID` field is a `FlatUID` - confirmed, per its own spec page, to be byte-identical to the same little-endian
|
|
6
|
+
* `Data1`/`Data2`/`Data3`+as-is-`Data4` layout `MapiGuid.ts` already implements, so no second GUID codec is
|
|
7
|
+
* needed here). */
|
|
8
|
+
export interface PropertyName {
|
|
9
|
+
guid: string;
|
|
10
|
+
kind: "lid" | "name";
|
|
11
|
+
lid?: number;
|
|
12
|
+
name?: string;
|
|
13
|
+
}
|
|
14
|
+
/** The most distinct named properties one session registers. The registry is saved with the session on every
|
|
15
|
+
* `Execute`, and a name can be up to 255 bytes, so letting a client fill the whole `0x8000`-`0xFFFF` ID space
|
|
16
|
+
* would make every request carry megabytes of session state. Real clients register a few hundred at most. */
|
|
17
|
+
export declare const MAX_NAMED_PROPERTIES_PER_SESSION = 4096;
|
|
18
|
+
/**
|
|
19
|
+
* Returns `propertyName`'s existing numeric property ID if an earlier `RopGetPropertyIdsFromNames` call in
|
|
20
|
+
* this session already assigned one, otherwise assigns and remembers the next free ID starting at
|
|
21
|
+
* `FIRST_NAMED_PROPERTY_ID`. This pragmatic subset always behaves as if the request's own `Flags` field
|
|
22
|
+
* requested "assign a new ID if unmapped" (`0x02`) - real Exchange's alternative (`0x00`, "only return
|
|
23
|
+
* already-mapped IDs") exists to let a client probe without committing a mailbox-wide registration; since this
|
|
24
|
+
* registry is already only ever session-scoped (not a real persisted per-mailbox mapping table), there is no
|
|
25
|
+
* meaningful difference between "probe" and "assign" here.
|
|
26
|
+
*
|
|
27
|
+
* Backed by `session.namedProperties`/`namedPropertyIds` (a forward and reverse map, kept in sync) and
|
|
28
|
+
* `session.nextNamedPropertyId`, an O(1) lookup/assignment pair rather than a linear scan plus a
|
|
29
|
+
* `Math.max(...spread)` over `Object.values(session.namedProperties)` - both real costs at scale: the spread
|
|
30
|
+
* form risks a `RangeError: Maximum call stack size exceeded` once a session has registered enough distinct
|
|
31
|
+
* names to exceed V8's function-argument-count limit, and the registry only ever grows for a session's
|
|
32
|
+
* lifetime, so a linear scan repeated once per property per row is quadratic over a session that resolves many
|
|
33
|
+
* named properties.
|
|
34
|
+
*
|
|
35
|
+
* Once `nextNamedPropertyId` would exceed `LAST_NAMED_PROPERTY_ID` (`MAX_NAMED_PROPERTIES_PER_SESSION` distinct
|
|
36
|
+
* names already registered this session), a *new*
|
|
37
|
+
* name can no longer be assigned a real ID; this returns `0x0000` for it instead of throwing, the exact value
|
|
38
|
+
* `[MS-OXCPRPT]`'s own `RopGetPropertyIdsFromNames` processing rules already use for "this `PropertyName`
|
|
39
|
+
* could not be resolved" (the same value this pragmatic subset already produces for a `Kind = 0xFF` entry) -
|
|
40
|
+
* not a new failure mode, just the same one applied to a different unmappable case. A name already registered
|
|
41
|
+
* before the registry filled up keeps returning its real, previously-assigned ID.
|
|
42
|
+
*/
|
|
43
|
+
export declare function assignOrGetNamedPropertyId(session: MapiSessionContext, propertyName: PropertyName): number;
|
|
44
|
+
/** The reverse lookup `RopSetProperties`/`RopGetPropertiesSpecific`/`RopQueryRows` use to recognize an incoming
|
|
45
|
+
* property ID `>= 0x8000` as one of this session's own mapped named properties - an O(1) lookup against
|
|
46
|
+
* `session.namedPropertyIds` (kept in sync by `assignOrGetNamedPropertyId`) rather than a linear scan of
|
|
47
|
+
* `session.namedProperties` with a `JSON.parse` per candidate - see that function's own doc comment for why
|
|
48
|
+
* this matters at scale, particularly since this is called once per named-property column per row from
|
|
49
|
+
* `RopQueryRows`. */
|
|
50
|
+
export declare function resolveNamedProperty(session: MapiSessionContext, propertyId: number): PropertyName | undefined;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { BufferWriter } from "../codec/BufferCursor.js";
|
|
2
|
+
import { PropertyType, PropertyValueData } from "../codec/PropertyValue.js";
|
|
3
|
+
import type { MapiSessionContext } from "../MapiSessionManager.js";
|
|
4
|
+
import { CalendarEventTargetInfo } from "./CalendarEventTarget.js";
|
|
5
|
+
import { ContactTargetInfo } from "./ContactTarget.js";
|
|
6
|
+
import { FolderResolutionCache, FolderTargetInfo } from "./FolderTarget.js";
|
|
7
|
+
import { MessageTargetInfo } from "./MessageTarget.js";
|
|
8
|
+
import type { RopContext } from "./RopHandler.js";
|
|
9
|
+
import { TaskTargetInfo } from "./TaskTarget.js";
|
|
10
|
+
/** A type-appropriate zero/empty value for a requested property this handler has no real data for - keeps
|
|
11
|
+
* `StandardPropertyRow` encoding valid (a real value of the right type, per `writePropertyValue`'s
|
|
12
|
+
* expectations) without needing to model every property a client could ever ask for. Shared by
|
|
13
|
+
* `RopQueryRowsHandler` (table rows) and `RopGetPropertiesSpecificHandler` (single-object property fetch). */
|
|
14
|
+
export declare function defaultValueForType(propertyType: PropertyType): PropertyValueData;
|
|
15
|
+
/**
|
|
16
|
+
* Encodes `value` as `propertyType` into `writer`, falling back to `defaultValueForType(propertyType)`'s own
|
|
17
|
+
* value if that throws - `RopQueryRowsHandler`/`RopGetPropertiesSpecificHandler`'s "an unsupported property
|
|
18
|
+
* falls back to a type-appropriate default, never an error" contract (see their own doc comments) was only true
|
|
19
|
+
* for a `propertyId` this codebase has no data for; it wasn't actually true for a `propertyId` it resolves fine
|
|
20
|
+
* whose value just doesn't fit the *client's own requested* `propertyType` for that column (`SetColumns` never
|
|
21
|
+
* cross-checks a column's declared type against what its resolver actually returns) - e.g. `PidTagSubject`'s
|
|
22
|
+
* plain string against a client-requested `PtypGuid` column throws inside `encodeGuid()`, well downstream of the
|
|
23
|
+
* `xxxValueFor` switch that only ever looks at `propertyId`. That escaped all the way to `RopDispatcher` and
|
|
24
|
+
* failed the *whole* ROP (discarding every row `RopQueryRows` had already built in that call) instead of just
|
|
25
|
+
* this one column degrading, the same as an actually-unmodeled property already does.
|
|
26
|
+
*
|
|
27
|
+
* Writes into a scratch `BufferWriter` first and only copies its bytes into `writer` once a whole value's worth
|
|
28
|
+
* encoded cleanly - several `PropertyType` cases (`PtypBinary`, every `PtypMultiple*`) write more than one field
|
|
29
|
+
* per value, and a value of the wrong *shape* (not just the wrong type) can throw partway through one of those,
|
|
30
|
+
* which would otherwise leave stray, wrongly-sized bytes in `writer` ahead of every column/row that follows -
|
|
31
|
+
* silent wire-format corruption worse than the original failure.
|
|
32
|
+
*
|
|
33
|
+
* `defaultValueForType(propertyType)` is built to match `writePropertyValue`'s own cases exactly, so the
|
|
34
|
+
* fallback only fails too when `propertyType` is a value neither function's `switch` recognizes at all (there's
|
|
35
|
+
* no way to encode a wire type this codec doesn't implement, and `RopSetColumns` never validates `propertyType`
|
|
36
|
+
* against the known enum) - that still throws, exactly as it already did before this function existed, since a
|
|
37
|
+
* genuinely unknown type isn't the "known type, mismatched value" case this exists to fix.
|
|
38
|
+
*/
|
|
39
|
+
export declare function writePropertyValueSafely(writer: BufferWriter, propertyType: PropertyType, value: PropertyValueData): void;
|
|
40
|
+
/** Resolves one requested property's value for a `"folder:<uid>"`/`"virtual:<name>"` target. */
|
|
41
|
+
export declare function folderValueFor(session: MapiSessionContext, propertyId: number, propertyType: PropertyType, target: string, info: FolderTargetInfo): PropertyValueData;
|
|
42
|
+
/** Resolves one requested property's value for a `"message:<uid>"` target. `labelNames` is this message's own
|
|
43
|
+
* `labelUids` already resolved to display names (see `resolvePropertyValues`'s own doc comment) - a plain
|
|
44
|
+
* parameter rather than a repo lookup here, since this function (like every other `xxxValueFor`) is
|
|
45
|
+
* synchronous and can't do its own async DB access. */
|
|
46
|
+
export declare function messageValueFor(session: MapiSessionContext, propertyId: number, propertyType: PropertyType, target: string, info: MessageTargetInfo, labelNames?: string[]): PropertyValueData;
|
|
47
|
+
/** Resolves one requested property's value for a `"calendarEvent:<uid>"` target. Almost every Appointment
|
|
48
|
+
* property is a *named* property (`PidLid*`, no fixed numeric ID) rather than a plain `PidTag*` - a property ID
|
|
49
|
+
* `>= 0x8000` is resolved back to its `(PropertySet GUID, LID)` identity via `resolveNamedProperty` before this
|
|
50
|
+
* can dispatch on it (see `NamedPropertyRegistry.ts`'s own doc comment and the architecture plan's "Calendar
|
|
51
|
+
* support" section for the full named-property table this switch implements). */
|
|
52
|
+
export declare function calendarEventValueFor(session: MapiSessionContext, propertyId: number, propertyType: PropertyType, target: string, info: CalendarEventTargetInfo, callerAddress: string): PropertyValueData;
|
|
53
|
+
/** Resolves one requested property's value for a `"contact:<uid>"` target - a `Folder` of type `CONTACTS`'s
|
|
54
|
+
* content-table rows/opened items. Every field here is a plain fixed `PidTag` (`[MS-OXOCNTC]`), unlike Calendar/
|
|
55
|
+
* Task's named-property tables, since Contact's own well-known fields all have real numeric IDs. */
|
|
56
|
+
export declare function contactValueFor(session: MapiSessionContext, propertyId: number, propertyType: PropertyType, target: string, info: ContactTargetInfo): PropertyValueData;
|
|
57
|
+
/** Resolves one requested property's value for a `"task:<uid>"` target - a `Folder` of type `TASKS`'s
|
|
58
|
+
* content-table rows/opened items. Almost every Task-specific property is a *named* property (`PidLid*`) under
|
|
59
|
+
* `PSETID_Task`, resolved back to its `(PropertySet GUID, LID)` identity via `resolveNamedProperty` exactly the
|
|
60
|
+
* same way `calendarEventValueFor` handles `PSETID_Appointment`/`PSETID_Common` - see `TaskNamedProperties.ts`'s
|
|
61
|
+
* own doc comment for the LID table this switch implements. */
|
|
62
|
+
export declare function taskValueFor(session: MapiSessionContext, propertyId: number, propertyType: PropertyType, target: string, info: TaskTargetInfo): PropertyValueData;
|
|
63
|
+
/** An optional, purely request-scoped (never persisted) memo shared across every `resolvePropertyValues` call
|
|
64
|
+
* within one ROP handler invocation - e.g. one `RopQueryRows` call resolving many table rows, or the (rarer)
|
|
65
|
+
* case of `RopGetPropertiesSpecific` resolving many columns for one object. Without it, both `allFolders` (via
|
|
66
|
+
* `resolveFolderInfo`'s own `hasChildren` computation) and `callerAddress` (the calendar organizer/attendee
|
|
67
|
+
* lookup below) would be re-fetched from the repo on every single row even though they can only ever have one
|
|
68
|
+
* value for the whole batch (this mailbox's folder list, this mailbox's own primary address) - a real,
|
|
69
|
+
* measurable cost for a hierarchy/calendar table with many rows, not just a theoretical one. A caller that
|
|
70
|
+
* resolves only a single target (`RopGetPropertiesSpecificHandler`) can simply omit this - resolving without a
|
|
71
|
+
* cache is still correct, just not deduplicated across calls it doesn't make anyway. */
|
|
72
|
+
export interface ResolutionCache extends FolderResolutionCache {
|
|
73
|
+
callerAddress?: string;
|
|
74
|
+
/** `Label.uid` -> `Label.name`, for the whole mailbox - built at most once per call the same way
|
|
75
|
+
* `allFolders`/`callerAddress` are, the first time a message row actually has a non-empty `labelUids` to
|
|
76
|
+
* resolve (most messages have none, so this stays unbuilt for a page with no labeled messages at all). */
|
|
77
|
+
labelNamesByUid?: Map<string, string>;
|
|
78
|
+
}
|
|
79
|
+
/** Resolves every column in `columns` for a single `target` (a `"folder:"`/`"virtual:"`/`"message:"`/
|
|
80
|
+
* `"calendarEvent:"`/`"contact:"`/`"task:"` target string), in order - the shared implementation behind both
|
|
81
|
+
* `RopQueryRowsHandler` (one call per table row) and `RopGetPropertiesSpecificHandler` (one call for the single
|
|
82
|
+
* object a handle refers to). */
|
|
83
|
+
export declare function resolvePropertyValues(target: string, columns: {
|
|
84
|
+
propertyId: number;
|
|
85
|
+
propertyType: PropertyType;
|
|
86
|
+
}[], context: Pick<RopContext, "mailboxUid" | "session" | "folderRepo" | "messageRepo" | "calendarEventRepo" | "mailboxRepo" | "contactRepo" | "taskRepo" | "labelRepo" | "budget">, cache?: ResolutionCache): Promise<PropertyValueData[]>;
|
|
@@ -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;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BufferReader, BufferWriter } from "../codec/BufferCursor.js";
|
|
2
|
+
import type { RopContext, RopHandler } from "./RopHandler.js";
|
|
3
|
+
/**
|
|
4
|
+
* `RopCreateMessage` (`[MS-OXCMSG]`/`[MS-OXCROPS]`): begins composing a new message in a folder, producing a
|
|
5
|
+
* new `"message"` Server object handle later ROPs (`RopSetProperties`, `RopOpenStream`, `RopSaveChangesMessage`)
|
|
6
|
+
* reference by `InputHandleIndex`. Unlike `RopOpenMessage`'s handle (an already-real, already-saved message),
|
|
7
|
+
* this handle's `entityUid` is deliberately left `""` - there is no real backing `Message` row, and won't be
|
|
8
|
+
* one until `RopSubmitMessage` actually sends it (this pragmatic subset has no separate Drafts-folder
|
|
9
|
+
* persistence step; see `RopSaveChangesMessageHandler`'s own doc comment). `draftFolderUid` remembers the
|
|
10
|
+
* target folder for that eventual send, and `draftProperties` starts empty, filled in by later
|
|
11
|
+
* `RopSetProperties`/`RopWriteStream` calls.
|
|
12
|
+
*
|
|
13
|
+
* `CodePageId` is decoded to advance past it correctly but not otherwise acted on (this pragmatic subset always
|
|
14
|
+
* encodes strings as UTF-16LE/UTF-8 directly, the same treatment `RopOpenMessageHandler` gives it).
|
|
15
|
+
* `AssociatedFlag` (FAI messages - hidden configuration/rules objects, not real mail) is decoded but not
|
|
16
|
+
* honored - this pragmatic subset's compose/send path only ever produces ordinary mail messages.
|
|
17
|
+
*
|
|
18
|
+
* @author Jean-Philippe Steinmetz
|
|
19
|
+
*/
|
|
20
|
+
export declare class RopCreateMessageHandler implements RopHandler {
|
|
21
|
+
readonly ropId = 6;
|
|
22
|
+
readonly responseHandleIndexOffset = 3;
|
|
23
|
+
handle(reader: BufferReader, writer: BufferWriter, context: RopContext): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { BufferReader, BufferWriter } from "../codec/BufferCursor.js";
|
|
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;
|
|
10
|
+
/**
|
|
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).
|
|
14
|
+
*
|
|
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.
|
|
19
|
+
*
|
|
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.
|
|
31
|
+
*
|
|
32
|
+
* The request's own `InputHandleIndex` (nominally the *parent* folder of the one being deleted) is validated to
|
|
33
|
+
* be a real, open folder handle but not cross-checked against the target folder's actual `parentFolderUid` -
|
|
34
|
+
* the same pragmatic simplification `RopOpenMessageHandler`'s own doc comment documents for its analogous
|
|
35
|
+
* `FolderId` field.
|
|
36
|
+
*
|
|
37
|
+
* @author Jean-Philippe Steinmetz
|
|
38
|
+
*/
|
|
39
|
+
export declare class RopDeleteFolderHandler implements RopHandler {
|
|
40
|
+
readonly ropId = 29;
|
|
41
|
+
handle(reader: BufferReader, writer: BufferWriter, context: RopContext): Promise<void>;
|
|
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;
|
|
53
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { BufferReader, BufferWriter } from "../codec/BufferCursor.js";
|
|
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
|
+
* Charged to `context.budget` like the `findOne`/`delete` calls around every call site of this function - a real
|
|
5
|
+
* DB `create()` whenever `context.audit` is configured (the case in every real deployment), so leaving it
|
|
6
|
+
* uncharged undercounted this loop's real DB work by a third. No-op (and no charge - there's no DB work to
|
|
7
|
+
* account for) when `context.audit` is absent, the same as an optional-repo item kind this pragmatic subset
|
|
8
|
+
* already skips charging for. */
|
|
9
|
+
export declare function auditMessageDelete(context: RopContext, message: {
|
|
10
|
+
uid: string;
|
|
11
|
+
mailboxUid: string;
|
|
12
|
+
subject?: string;
|
|
13
|
+
folderUid: string;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* `RopDeleteMessages` (`[MS-OXCMSG]`/`[MS-OXCROPS]`, RopId `0x1E`): deletes one or more messages by `MessageId`
|
|
17
|
+
* from an already-open folder handle. Reuses `RecoverableRepoUtils.delete()` exactly as-built in Phase 2 - the
|
|
18
|
+
* same soft-delete-with-watermark-bump machinery EAS's own `Sync`/`FolderSync` incremental-delete detection
|
|
19
|
+
* already relies on (see `BaseMapiEmsmdbRoute.ts`'s own doc comment for why `folderRepo`/`messageRepo`/
|
|
20
|
+
* `calendarEventRepo` are built as `RecoverableRepoUtils`, not plain `RepoUtils`) - no new deletion-tracking
|
|
21
|
+
* code, zero duplication.
|
|
22
|
+
*
|
|
23
|
+
* A `MessageId` resolves via `session.messageIds` (`MessageTarget.assignOrGetMid`'s own registry, already
|
|
24
|
+
* generic over both `"message:<uid>"` and `"calendarEvent:<uid>"` target strings, since a Calendar item is
|
|
25
|
+
* itself a Message object on the wire - see `CalendarEventTarget.ts`'s own doc comment) directly to the entity
|
|
26
|
+
* to delete, the same "the session already knows what this small integer refers to" design every other MID-
|
|
27
|
+
* consuming ROP in this pragmatic subset already uses (`RopOpenMessage`, `RopSaveChangesMessage`) - the
|
|
28
|
+
* request's own `InputHandleIndex` (nominally the messages' shared parent folder) is validated to be a real,
|
|
29
|
+
* open folder handle but not cross-checked against each message's actual `folderUid`, the same pragmatic
|
|
30
|
+
* simplification `RopOpenMessageHandler`'s own doc comment documents for its analogous `FolderId` field.
|
|
31
|
+
*
|
|
32
|
+
* `WantAsynchronous` (`RopProgress`-reported async completion) and `NotifyNonRead` (non-read-receipt generation
|
|
33
|
+
* to the original sender) are decoded to advance the reader correctly but not honored - this pragmatic subset
|
|
34
|
+
* has no async-ROP or non-read-receipt machinery. `PartialCompletion` is reported `true` if any requested
|
|
35
|
+
* `MessageId` didn't resolve to a real message/calendar-event target - the rest are still deleted, matching
|
|
36
|
+
* this pragmatic subset's "don't fail the whole ROP over one stale reference" principle used throughout.
|
|
37
|
+
*
|
|
38
|
+
* @author Jean-Philippe Steinmetz
|
|
39
|
+
*/
|
|
40
|
+
export declare class RopDeleteMessagesHandler implements RopHandler {
|
|
41
|
+
readonly ropId = 30;
|
|
42
|
+
handle(reader: BufferReader, writer: BufferWriter, context: RopContext): Promise<void>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BufferReader, BufferWriter } from "../codec/BufferCursor.js";
|
|
2
|
+
import type { RopContext, RopHandler } from "./RopHandler.js";
|
|
3
|
+
/**
|
|
4
|
+
* `RopFastTransferSourceCopyProperties` (`[MS-OXCFXICS]`/`[MS-OXCROPS]`, RopId `0x69`): the explicit-column
|
|
5
|
+
* sibling of `RopFastTransferSourceCopyTo` - `PropertyTags` here is the properties to **copy** (an include
|
|
6
|
+
* list, per `[MS-OXCROPS]`'s own wording, the opposite of `CopyTo`'s exclude list), applied uniformly at both
|
|
7
|
+
* folder- and message-level wherever `FastTransferStream.ts` builds a `propList` - see that file's own doc
|
|
8
|
+
* comment for this pragmatic subset's overall ICS scope. An empty `PropertyTags` list is honored literally
|
|
9
|
+
* (copies zero properties, i.e. empty `propList`s) rather than falling back to `CopyTo`'s own default column
|
|
10
|
+
* set - that fallback belongs to `CopyTo` alone, since an explicit "copy nothing" request here is spec-valid
|
|
11
|
+
* and distinguishable from "use the default set" (`CopyTo` simply has no `PropertyTags`-supplied alternative to
|
|
12
|
+
* fall back from).
|
|
13
|
+
*
|
|
14
|
+
* `Level`/`CopyFlags`/`SendOptions` are decoded to advance the reader correctly but not honored - see
|
|
15
|
+
* `RopFastTransferSourceCopyToHandler`'s own doc comment for the identical reasoning. Note `CopyFlags` here is
|
|
16
|
+
* **1 byte**, not 4 like `CopyTo`'s own field of the same name - confirmed against each ROP's own request-buffer
|
|
17
|
+
* page separately, not assumed identical.
|
|
18
|
+
*
|
|
19
|
+
* @author Jean-Philippe Steinmetz
|
|
20
|
+
*/
|
|
21
|
+
export declare class RopFastTransferSourceCopyPropertiesHandler implements RopHandler {
|
|
22
|
+
readonly ropId = 105;
|
|
23
|
+
readonly responseHandleIndexOffset = 3;
|
|
24
|
+
handle(reader: BufferReader, writer: BufferWriter, context: RopContext): Promise<void>;
|
|
25
|
+
}
|