microsoft-graph-client 1.0.51 → 1.0.53
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/dist/api/calendar.d.ts.map +1 -1
- package/dist/api/calendar.js +6 -1
- package/dist/api/calendar.js.map +1 -1
- package/dist/api/chats.d.ts.map +1 -1
- package/dist/api/chats.js +17 -18
- package/dist/api/chats.js.map +1 -1
- package/dist/api/mail.d.ts.map +1 -1
- package/dist/api/mail.js +21 -6
- package/dist/api/mail.js.map +1 -1
- package/dist/api/mailFolders.d.ts +5 -1
- package/dist/api/mailFolders.d.ts.map +1 -1
- package/dist/api/mailFolders.js +48 -4
- package/dist/api/mailFolders.js.map +1 -1
- package/dist/api/messages.d.ts +9 -0
- package/dist/api/messages.d.ts.map +1 -1
- package/dist/api/messages.js +28 -1
- package/dist/api/messages.js.map +1 -1
- package/dist/api/transcripts.d.ts.map +1 -1
- package/dist/api/transcripts.js +7 -1
- package/dist/api/transcripts.js.map +1 -1
- package/dist/api/users.d.ts.map +1 -1
- package/dist/api/users.js +5 -1
- package/dist/api/users.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types/mail.d.ts +3 -2
- package/dist/types/mail.d.ts.map +1 -1
- package/dist/types/mailFolder.d.ts +39 -1
- package/dist/types/mailFolder.d.ts.map +1 -1
- package/dist/types/message.d.ts +12 -0
- package/dist/types/message.d.ts.map +1 -1
- package/dist/utils/index.d.ts +4 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/odata.d.ts +118 -19
- package/dist/utils/odata.d.ts.map +1 -1
- package/dist/utils/odata.js +136 -20
- package/dist/utils/odata.js.map +1 -1
- package/dist/utils/retention.d.ts +59 -0
- package/dist/utils/retention.d.ts.map +1 -0
- package/dist/utils/retention.js +132 -0
- package/dist/utils/retention.js.map +1 -0
- package/package.json +1 -1
package/dist/utils/odata.js
CHANGED
|
@@ -19,9 +19,21 @@
|
|
|
19
19
|
* `?$filter=...'fm & fd'...` and the server parsed the `&` as a query
|
|
20
20
|
* param separator. See test/probe-encoding.mts for the verification.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* **Build filter and search strings with the `odataFilter` / `odataSearch`
|
|
23
|
+
* tagged templates.** The tag applies the right layers to every interpolation,
|
|
24
|
+
* so the safe form is the default form instead of something each call site has
|
|
25
|
+
* to remember:
|
|
26
|
+
*
|
|
27
|
+
* filters.push(odataFilter`from/emailAddress/address eq '${params.from}'`);
|
|
28
|
+
*
|
|
29
|
+
* Remembering to apply `odataFilterValue` by hand is exactly what let five call
|
|
30
|
+
* sites interpolate raw values for months. An untagged template
|
|
31
|
+
* literal that opens an OData string literal, or that reaches `.filter()` /
|
|
32
|
+
* `.search()` / `.query()` directly, is therefore an eslint error inside
|
|
33
|
+
* `src/api`.
|
|
34
|
+
*
|
|
35
|
+
* The value helpers below stay exported: they are what the tags are built from,
|
|
36
|
+
* and consumers of this package building their own queries need them.
|
|
25
37
|
*
|
|
26
38
|
* @see https://learn.microsoft.com/en-us/graph/filter-query-parameter
|
|
27
39
|
*/
|
|
@@ -29,8 +41,8 @@
|
|
|
29
41
|
* Escapes single quotes in an OData string literal.
|
|
30
42
|
*
|
|
31
43
|
* Use this only if you are constructing a fully-formed URL yourself and your
|
|
32
|
-
* URL builder will handle URL encoding separately. In most cases, prefer
|
|
33
|
-
* `
|
|
44
|
+
* URL builder will handle URL encoding separately. In most cases, prefer the
|
|
45
|
+
* `odataFilter` tag, which combines both layers.
|
|
34
46
|
*
|
|
35
47
|
* @example
|
|
36
48
|
* escapeODataLiteral("Let's Talk") // → "Let''s Talk"
|
|
@@ -46,29 +58,133 @@ export function escapeODataLiteral(value) {
|
|
|
46
58
|
* 1. OData literal escape (`'` → `''`)
|
|
47
59
|
* 2. URL-encode the result so reserved characters don't break the request URL
|
|
48
60
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* const value = odataFilterValue("FM & FD");
|
|
52
|
-
* client.api('/me/chats').filter(`contains(tolower(topic), '${value}')`).get();
|
|
53
|
-
* // The SDK sends: ?$filter=contains(tolower(topic), 'fm%20%26%20fd')
|
|
61
|
+
* Prefer the `odataFilter` tag inside this package; reach for this directly
|
|
62
|
+
* only when you are assembling a value away from the string it lands in.
|
|
54
63
|
*
|
|
55
64
|
* @example
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* //
|
|
65
|
+
* odataFilterValue('invoice+statements@example.com');
|
|
66
|
+
* // → 'invoice%2Bstatements%40example.com'
|
|
67
|
+
* // Without the encoding, the SDK sends a raw `+`, which the server reads as
|
|
68
|
+
* // a space and the filter matches nobody.
|
|
60
69
|
*/
|
|
61
70
|
export function odataFilterValue(value) {
|
|
62
71
|
return encodeURIComponent(escapeODataLiteral(value));
|
|
63
72
|
}
|
|
64
73
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
74
|
+
* Prepares a string for safe use as a `$search` value passed through the
|
|
75
|
+
* Microsoft Graph SDK's `.search()` builder.
|
|
76
|
+
*
|
|
77
|
+
* The SDK's `.search()` builder has the exact same non-encoding behavior as
|
|
78
|
+
* `.filter()` (see the module doc above) — it writes the value straight into
|
|
79
|
+
* `$search=...` on the request URL with no URL-encoding. `$search` values are
|
|
80
|
+
* plain quoted phrases, not OData string literals, so there's no literal
|
|
81
|
+
* layer to apply here (unlike `odataFilterValue`) — this is URL-encoding
|
|
82
|
+
* only.
|
|
83
|
+
*
|
|
84
|
+
* **It does not neutralize a literal `"` in the value.** The server percent-
|
|
85
|
+
* decodes `$search` before parsing it as KQL, so an embedded quote is restored
|
|
86
|
+
* and closes the phrase the caller wrapped around it. Verified against Graph:
|
|
87
|
+
* searching `Anthropic" OR from:someone@example.com` comes back as
|
|
88
|
+
* `Syntax error: character ':' is not valid at position 19 in
|
|
89
|
+
* '"Anthropic" OR from:someone@example.com"'` — the server quoting back the
|
|
90
|
+
* decoded string. That fails loud rather than mis-parsing silently, but a
|
|
91
|
+
* caller needing to search for a literal quote cannot express it here. What
|
|
92
|
+
* the encoding does buy is `+`, `&` and `#` surviving the URL.
|
|
93
|
+
*
|
|
94
|
+
* Prefer the `odataSearch` tag inside this package.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* odataSearchValue('from:invoice+statements@example.com');
|
|
98
|
+
* // → 'from%3Ainvoice%2Bstatements%40example.com'
|
|
99
|
+
*/
|
|
100
|
+
export function odataSearchValue(value) {
|
|
101
|
+
return encodeURIComponent(value);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* An already-built OData fragment, interpolated into a tag verbatim.
|
|
105
|
+
*
|
|
106
|
+
* Produced only by {@link odataRaw}. Carrying a nominal type rather than a bare
|
|
107
|
+
* string is the point: the tags encode anything they are handed, and the only
|
|
108
|
+
* way past that is to say so explicitly at the call site.
|
|
109
|
+
*/
|
|
110
|
+
export class ODataRaw {
|
|
111
|
+
fragment;
|
|
112
|
+
constructor(fragment) {
|
|
113
|
+
this.fragment = fragment;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Marks an interpolation as OData *syntax* rather than a *value*, so the tags
|
|
118
|
+
* pass it through unencoded.
|
|
119
|
+
*
|
|
120
|
+
* This is the escape hatch, and it exists for one legitimate shape: composing
|
|
121
|
+
* clauses that the tags already built, where the parentheses, quotes and
|
|
122
|
+
* operators are structure and encoding them would destroy the expression.
|
|
123
|
+
*
|
|
124
|
+
* Never hand it something a caller supplied. If a value reaches `odataRaw`,
|
|
125
|
+
* the encoding is gone and nothing else will catch it.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* const clauses = addresses.map((a) => odataFilter`from/emailAddress/address eq '${a}'`);
|
|
129
|
+
* const anyOf = odataFilter`(${odataRaw(clauses.join(' or '))})`;
|
|
130
|
+
*/
|
|
131
|
+
export function odataRaw(fragment) {
|
|
132
|
+
return new ODataRaw(fragment);
|
|
133
|
+
}
|
|
134
|
+
function interpolate(strings, values, encode) {
|
|
135
|
+
let out = strings[0];
|
|
136
|
+
for (let i = 0; i < values.length; i++) {
|
|
137
|
+
const value = values[i];
|
|
138
|
+
out += value instanceof ODataRaw ? value.fragment : encode(String(value));
|
|
139
|
+
out += strings[i + 1];
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Builds an OData `$filter` expression, escaping and URL-encoding every
|
|
145
|
+
* interpolated value (see {@link odataFilterValue}).
|
|
68
146
|
*
|
|
69
|
-
*
|
|
147
|
+
* Write the OData syntax — including the quotes around a string literal — in
|
|
148
|
+
* the template; interpolate only values. Numbers, booleans and unquoted
|
|
149
|
+
* datetime literals are safe to interpolate as-is: encoding leaves them intact
|
|
150
|
+
* apart from characters that genuinely have to be encoded, such as the `+` in a
|
|
151
|
+
* timezone offset.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* odataFilter`from/emailAddress/address eq '${'invoice+statements@example.com'}'`;
|
|
155
|
+
* // → "from/emailAddress/address eq 'invoice%2Bstatements%40example.com'"
|
|
156
|
+
*/
|
|
157
|
+
export function odataFilter(strings, ...values) {
|
|
158
|
+
return interpolate(strings, values, odataFilterValue);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Builds an OData `$search` expression, URL-encoding every interpolated value
|
|
162
|
+
* (see {@link odataSearchValue}).
|
|
163
|
+
*
|
|
164
|
+
* Write the phrase quotes and any KQL operators in the template; interpolate
|
|
165
|
+
* only values.
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* odataSearch`"mail:${'a+b@example.com'}" OR "displayName:${'a+b@example.com'}"`;
|
|
169
|
+
* // → '"mail:a%2Bb%40example.com" OR "displayName:a%2Bb%40example.com"'
|
|
170
|
+
*/
|
|
171
|
+
export function odataSearch(strings, ...values) {
|
|
172
|
+
return interpolate(strings, values, odataSearchValue);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Joins already-built clauses into an any-of group, parenthesised only when
|
|
176
|
+
* there is more than one.
|
|
177
|
+
*
|
|
178
|
+
* The parentheses matter: an `or` group spliced into a filter that also has
|
|
179
|
+
* `and` clauses would otherwise bind wrongly and widen the query instead of
|
|
180
|
+
* narrowing it. This exists so that composing clauses — the one legitimate use
|
|
181
|
+
* of {@link odataRaw} — has a name, rather than each new any-of filter reaching
|
|
182
|
+
* for the escape hatch and copying the last call site that did.
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* odataAnyOf(addresses.map((a) => odataFilter`from/emailAddress/address eq '${a}'`));
|
|
70
186
|
*/
|
|
71
|
-
export function
|
|
72
|
-
return
|
|
187
|
+
export function odataAnyOf(clauses) {
|
|
188
|
+
return clauses.length === 1 ? clauses[0] : odataFilter `(${odataRaw(clauses.join(' or '))})`;
|
|
73
189
|
}
|
|
74
190
|
//# sourceMappingURL=odata.js.map
|
package/dist/utils/odata.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"odata.js","sourceRoot":"","sources":["../../src/utils/odata.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"odata.js","sourceRoot":"","sources":["../../src/utils/odata.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,OAAO,kBAAkB,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC;AACvD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,OAAO,kBAAkB,CAAC,KAAK,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,OAAO,QAAQ;IACgB;IAAnC,YAAmC,QAAgB;QAAhB,aAAQ,GAAR,QAAQ,CAAQ;IAAG,CAAC;CACxD;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,QAAQ,CAAC,QAAgB;IACvC,OAAO,IAAI,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC;AAKD,SAAS,WAAW,CAClB,OAA6B,EAC7B,MAA4B,EAC5B,MAAiC;IAEjC,IAAI,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACxB,GAAG,IAAI,KAAK,YAAY,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1E,GAAG,IAAI,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACxB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,WAAW,CAAC,OAA6B,EAAE,GAAG,MAA4B;IACxF,OAAO,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CAAC,OAA6B,EAAE,GAAG,MAA4B;IACxF,OAAO,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,UAAU,CAAC,OAAiB;IAC1C,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAA,IAAI,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC;AAC9F,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** MAPI id of PR_POLICY_TAG — the retention tag GUID, stored as bytes. */
|
|
2
|
+
export declare const RETENTION_TAG_PROPERTY = "Binary 0x3019";
|
|
3
|
+
/** MAPI id of PR_RETENTION_PERIOD — days before the tag's action applies. */
|
|
4
|
+
export declare const RETENTION_PERIOD_PROPERTY = "Integer 0x301A";
|
|
5
|
+
/** MAPI id of PR_RETENTION_FLAGS — Exchange bookkeeping; read-only in practice. */
|
|
6
|
+
export declare const RETENTION_FLAGS_PROPERTY = "Integer 0x301D";
|
|
7
|
+
/** `$filter` selecting exactly the three retention properties in an `$expand`. */
|
|
8
|
+
export declare const RETENTION_PROPERTY_FILTER: string;
|
|
9
|
+
/** One entry of a `mailFolder`'s `singleValueExtendedProperties` collection, as Graph returns it. */
|
|
10
|
+
export interface SingleValueExtendedProperty {
|
|
11
|
+
id?: string | null;
|
|
12
|
+
value?: string | null;
|
|
13
|
+
}
|
|
14
|
+
/** One entry as it is *sent* — a `null` value is the delete, so it is not optional. */
|
|
15
|
+
export interface RetentionPropertyPayload {
|
|
16
|
+
id: string;
|
|
17
|
+
value: string | null;
|
|
18
|
+
}
|
|
19
|
+
/** A folder's retention policy, in the terms a person set it in. */
|
|
20
|
+
export interface RetentionPolicy {
|
|
21
|
+
/** Retention tag GUID, in the form Exchange shows it. */
|
|
22
|
+
tag: string;
|
|
23
|
+
/** Days before the tag's action applies, when the folder carries one. */
|
|
24
|
+
periodDays: number | null;
|
|
25
|
+
/** Raw PR_RETENTION_FLAGS. Reported because it is there, not because it means anything. */
|
|
26
|
+
flags: number | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Decodes PR_POLICY_TAG's bytes into a GUID string.
|
|
30
|
+
*
|
|
31
|
+
* The bytes are in .NET `Guid` layout — the first three fields little-endian,
|
|
32
|
+
* the last two big-endian — not a flat big-endian read. Reading them flat
|
|
33
|
+
* produces a string that looks like a GUID and is not one: tags read off a real
|
|
34
|
+
* mailbox decode to a valid version-4 GUID under this layout, and to an
|
|
35
|
+
* impossible version nibble when the bytes are read straight through.
|
|
36
|
+
*/
|
|
37
|
+
export declare function decodeRetentionTag(base64Value: string): string | null;
|
|
38
|
+
/** Encodes a GUID string back into PR_POLICY_TAG's bytes. Inverse of {@link decodeRetentionTag}. */
|
|
39
|
+
export declare function encodeRetentionTag(guid: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Reads a folder's retention policy out of its expanded extended properties.
|
|
42
|
+
*
|
|
43
|
+
* Returns `null` when the folder carries no tag. A folder whose policy has been
|
|
44
|
+
* cleared keeps a `PR_RETENTION_FLAGS` of 128, so presence of the properties is
|
|
45
|
+
* not the test — presence of a tag is.
|
|
46
|
+
*/
|
|
47
|
+
export declare function parseRetentionPolicy(properties: SingleValueExtendedProperty[] | null | undefined): RetentionPolicy | null;
|
|
48
|
+
/**
|
|
49
|
+
* Builds the `singleValueExtendedProperties` payload for a PATCH.
|
|
50
|
+
*
|
|
51
|
+
* Pass a tag and period to assign a policy, or `null` for both to clear one.
|
|
52
|
+
* `PR_RETENTION_FLAGS` is only ever sent as `null`, on a clear: Exchange owns
|
|
53
|
+
* that value and rewrites whatever is sent.
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildRetentionProperties(policy: {
|
|
56
|
+
tag: string | null;
|
|
57
|
+
periodDays: number | null;
|
|
58
|
+
}): RetentionPropertyPayload[];
|
|
59
|
+
//# sourceMappingURL=retention.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retention.d.ts","sourceRoot":"","sources":["../../src/utils/retention.ts"],"names":[],"mappings":"AA6BA,0EAA0E;AAC1E,eAAO,MAAM,sBAAsB,kBAAkB,CAAC;AACtD,6EAA6E;AAC7E,eAAO,MAAM,yBAAyB,mBAAmB,CAAC;AAC1D,mFAAmF;AACnF,eAAO,MAAM,wBAAwB,mBAAmB,CAAC;AAEzD,kFAAkF;AAClF,eAAO,MAAM,yBAAyB,QAUvB,CAAC;AAEhB,qGAAqG;AACrG,MAAM,WAAW,2BAA2B;IAC1C,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,uFAAuF;AACvF,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,oEAAoE;AACpE,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC;IACZ,yEAAyE;IACzE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,2FAA2F;IAC3F,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAID;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQrE;AAED,oGAAoG;AACpG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAUvD;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,2BAA2B,EAAE,GAAG,IAAI,GAAG,SAAS,GAC3D,eAAe,GAAG,IAAI,CAsBxB;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,GAAG,wBAAwB,EAAE,CAiB7B"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mail folder retention policies — Outlook's "Assign Policy".
|
|
3
|
+
*
|
|
4
|
+
* There is no `retentionPolicy` property on `mailFolder`. The policy lives in
|
|
5
|
+
* three MAPI properties reachable through the `singleValueExtendedProperties`
|
|
6
|
+
* navigation property, which is why a scan of the declared `<Property>` list in
|
|
7
|
+
* `$metadata` concludes the feature is missing:
|
|
8
|
+
*
|
|
9
|
+
* PR_POLICY_TAG `Binary 0x3019` the retention tag, a GUID in bytes
|
|
10
|
+
* PR_RETENTION_PERIOD `Integer 0x301A` days before the tag's action applies
|
|
11
|
+
* PR_RETENTION_FLAGS `Integer 0x301D` Exchange's own bookkeeping
|
|
12
|
+
*
|
|
13
|
+
* Three behaviours verified against a live mailbox, each of which shapes the
|
|
14
|
+
* code below:
|
|
15
|
+
*
|
|
16
|
+
* - **Flags do not round-trip.** Write 9, read back 137: Exchange ORs in 128
|
|
17
|
+
* (NeedsRescan) itself. Never assert on them, and never send them back.
|
|
18
|
+
* - **Clearing works only with `value: null`.** An empty string is stored as
|
|
19
|
+
* an empty string, so the property survives and the folder still looks
|
|
20
|
+
* policed. After a clear, `PR_RETENTION_FLAGS` is left behind as 128, so a
|
|
21
|
+
* folder is "policed" when it has a tag, not when it has any of the three.
|
|
22
|
+
* - **An invented tag GUID is accepted silently.** The PATCH succeeds, the
|
|
23
|
+
* value reads back perfectly, and the Managed Folder Assistant then ignores
|
|
24
|
+
* it forever. Every observable signal says a dead policy is live, which is
|
|
25
|
+
* why callers must validate a tag against the ones a mailbox actually uses
|
|
26
|
+
* rather than trusting a successful write.
|
|
27
|
+
*/
|
|
28
|
+
import { odataFilter, odataRaw } from './odata.js';
|
|
29
|
+
/** MAPI id of PR_POLICY_TAG — the retention tag GUID, stored as bytes. */
|
|
30
|
+
export const RETENTION_TAG_PROPERTY = 'Binary 0x3019';
|
|
31
|
+
/** MAPI id of PR_RETENTION_PERIOD — days before the tag's action applies. */
|
|
32
|
+
export const RETENTION_PERIOD_PROPERTY = 'Integer 0x301A';
|
|
33
|
+
/** MAPI id of PR_RETENTION_FLAGS — Exchange bookkeeping; read-only in practice. */
|
|
34
|
+
export const RETENTION_FLAGS_PROPERTY = 'Integer 0x301D';
|
|
35
|
+
/** `$filter` selecting exactly the three retention properties in an `$expand`. */
|
|
36
|
+
export const RETENTION_PROPERTY_FILTER = [RETENTION_TAG_PROPERTY, RETENTION_PERIOD_PROPERTY, RETENTION_FLAGS_PROPERTY]
|
|
37
|
+
// odataRaw because these are our own constants, not caller values: encoding
|
|
38
|
+
// them would send `Binary%200x3019`, which matches no property. Going through
|
|
39
|
+
// the tag anyway keeps the last hand-built OData string out of the package
|
|
40
|
+
// that exists to stop hand-built OData strings.
|
|
41
|
+
//
|
|
42
|
+
// Joined rather than composed with odataAnyOf: this sits inside
|
|
43
|
+
// `$expand=singleValueExtendedProperties($filter=…)`, where the parentheses
|
|
44
|
+
// odataAnyOf adds would be a change to a request that already works.
|
|
45
|
+
.map((id) => odataFilter `id eq '${odataRaw(id)}'`)
|
|
46
|
+
.join(' or ');
|
|
47
|
+
const GUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
48
|
+
/**
|
|
49
|
+
* Decodes PR_POLICY_TAG's bytes into a GUID string.
|
|
50
|
+
*
|
|
51
|
+
* The bytes are in .NET `Guid` layout — the first three fields little-endian,
|
|
52
|
+
* the last two big-endian — not a flat big-endian read. Reading them flat
|
|
53
|
+
* produces a string that looks like a GUID and is not one: tags read off a real
|
|
54
|
+
* mailbox decode to a valid version-4 GUID under this layout, and to an
|
|
55
|
+
* impossible version nibble when the bytes are read straight through.
|
|
56
|
+
*/
|
|
57
|
+
export function decodeRetentionTag(base64Value) {
|
|
58
|
+
const bytes = Buffer.from(base64Value, 'base64');
|
|
59
|
+
if (bytes.length !== 16)
|
|
60
|
+
return null;
|
|
61
|
+
// `bytes` is freshly allocated per call, so reversing sub-ranges in place is
|
|
62
|
+
// contained, and each range is reversed exactly once.
|
|
63
|
+
const hex = (start, end, littleEndian) => (littleEndian ? bytes.subarray(start, end).reverse() : bytes.subarray(start, end)).toString('hex');
|
|
64
|
+
return [hex(0, 4, true), hex(4, 6, true), hex(6, 8, true), hex(8, 10, false), hex(10, 16, false)].join('-');
|
|
65
|
+
}
|
|
66
|
+
/** Encodes a GUID string back into PR_POLICY_TAG's bytes. Inverse of {@link decodeRetentionTag}. */
|
|
67
|
+
export function encodeRetentionTag(guid) {
|
|
68
|
+
if (!GUID_PATTERN.test(guid)) {
|
|
69
|
+
throw new Error(`Not a GUID: "${guid}". A retention tag is a GUID, e.g. 1b2c3d4e-5f60-4a7b-8c9d-0e1f2a3b4c5d.`);
|
|
70
|
+
}
|
|
71
|
+
const bytes = Buffer.from(guid.replace(/-/g, ''), 'hex');
|
|
72
|
+
// The first three fields are little-endian; the last two are already in order.
|
|
73
|
+
bytes.subarray(0, 4).reverse();
|
|
74
|
+
bytes.subarray(4, 6).reverse();
|
|
75
|
+
bytes.subarray(6, 8).reverse();
|
|
76
|
+
return bytes.toString('base64');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Reads a folder's retention policy out of its expanded extended properties.
|
|
80
|
+
*
|
|
81
|
+
* Returns `null` when the folder carries no tag. A folder whose policy has been
|
|
82
|
+
* cleared keeps a `PR_RETENTION_FLAGS` of 128, so presence of the properties is
|
|
83
|
+
* not the test — presence of a tag is.
|
|
84
|
+
*/
|
|
85
|
+
export function parseRetentionPolicy(properties) {
|
|
86
|
+
if (!properties?.length)
|
|
87
|
+
return null;
|
|
88
|
+
// Graph lowercases the hex in the ids it returns (`Integer 0x301a`), so match
|
|
89
|
+
// case-insensitively rather than against the constants verbatim.
|
|
90
|
+
const find = (id) => {
|
|
91
|
+
const wanted = id.toLowerCase();
|
|
92
|
+
return properties.find((p) => p.id?.toLowerCase() === wanted)?.value ?? null;
|
|
93
|
+
};
|
|
94
|
+
const rawTag = find(RETENTION_TAG_PROPERTY);
|
|
95
|
+
if (!rawTag)
|
|
96
|
+
return null;
|
|
97
|
+
const tag = decodeRetentionTag(rawTag);
|
|
98
|
+
if (!tag)
|
|
99
|
+
return null;
|
|
100
|
+
const toInt = (value) => {
|
|
101
|
+
if (value === null || value === '')
|
|
102
|
+
return null;
|
|
103
|
+
const n = Number.parseInt(value, 10);
|
|
104
|
+
return Number.isNaN(n) ? null : n;
|
|
105
|
+
};
|
|
106
|
+
return { tag, periodDays: toInt(find(RETENTION_PERIOD_PROPERTY)), flags: toInt(find(RETENTION_FLAGS_PROPERTY)) };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Builds the `singleValueExtendedProperties` payload for a PATCH.
|
|
110
|
+
*
|
|
111
|
+
* Pass a tag and period to assign a policy, or `null` for both to clear one.
|
|
112
|
+
* `PR_RETENTION_FLAGS` is only ever sent as `null`, on a clear: Exchange owns
|
|
113
|
+
* that value and rewrites whatever is sent.
|
|
114
|
+
*/
|
|
115
|
+
export function buildRetentionProperties(policy) {
|
|
116
|
+
if (policy.tag === null) {
|
|
117
|
+
return [
|
|
118
|
+
{ id: RETENTION_TAG_PROPERTY, value: null },
|
|
119
|
+
{ id: RETENTION_PERIOD_PROPERTY, value: null },
|
|
120
|
+
{ id: RETENTION_FLAGS_PROPERTY, value: null },
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
return [
|
|
124
|
+
{ id: RETENTION_TAG_PROPERTY, value: encodeRetentionTag(policy.tag) },
|
|
125
|
+
// Always sent, `null` included. Omitting it leaves whatever period the
|
|
126
|
+
// previous policy wrote, so assigning a "delete after 7 days" tag to a
|
|
127
|
+
// folder that carried a 30-day one would produce a folder reporting the new
|
|
128
|
+
// tag with the old period — a mismatch every read reports as intentional.
|
|
129
|
+
{ id: RETENTION_PERIOD_PROPERTY, value: policy.periodDays === null ? null : String(policy.periodDays) },
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=retention.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retention.js","sourceRoot":"","sources":["../../src/utils/retention.ts"],"names":[],"mappings":"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"}
|