m365-agent-cli 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +916 -0
  3. package/package.json +50 -0
  4. package/src/cli.ts +100 -0
  5. package/src/commands/auto-reply.ts +182 -0
  6. package/src/commands/calendar.ts +576 -0
  7. package/src/commands/counter.ts +87 -0
  8. package/src/commands/create-event.ts +544 -0
  9. package/src/commands/delegates.ts +286 -0
  10. package/src/commands/delete-event.ts +321 -0
  11. package/src/commands/drafts.ts +502 -0
  12. package/src/commands/files.ts +532 -0
  13. package/src/commands/find.ts +195 -0
  14. package/src/commands/findtime.ts +270 -0
  15. package/src/commands/folders.ts +177 -0
  16. package/src/commands/forward-event.ts +49 -0
  17. package/src/commands/graph-calendar.ts +217 -0
  18. package/src/commands/login.ts +195 -0
  19. package/src/commands/mail.ts +950 -0
  20. package/src/commands/oof.ts +263 -0
  21. package/src/commands/outlook-categories.ts +173 -0
  22. package/src/commands/outlook-graph.ts +880 -0
  23. package/src/commands/planner.ts +1678 -0
  24. package/src/commands/respond.ts +291 -0
  25. package/src/commands/rooms.ts +210 -0
  26. package/src/commands/rules.ts +511 -0
  27. package/src/commands/schedule.ts +109 -0
  28. package/src/commands/send.ts +204 -0
  29. package/src/commands/serve.ts +14 -0
  30. package/src/commands/sharepoint.ts +179 -0
  31. package/src/commands/site-pages.ts +163 -0
  32. package/src/commands/subscribe.ts +103 -0
  33. package/src/commands/subscriptions.ts +29 -0
  34. package/src/commands/suggest.ts +155 -0
  35. package/src/commands/todo.ts +2092 -0
  36. package/src/commands/update-event.ts +608 -0
  37. package/src/commands/update.ts +88 -0
  38. package/src/commands/verify-token.ts +62 -0
  39. package/src/commands/whoami.ts +74 -0
  40. package/src/index.ts +190 -0
  41. package/src/lib/atomic-write.ts +20 -0
  42. package/src/lib/attach-link-spec.test.ts +24 -0
  43. package/src/lib/attach-link-spec.ts +70 -0
  44. package/src/lib/attachments.ts +79 -0
  45. package/src/lib/auth.ts +192 -0
  46. package/src/lib/calendar-range.test.ts +41 -0
  47. package/src/lib/calendar-range.ts +103 -0
  48. package/src/lib/dates.test.ts +74 -0
  49. package/src/lib/dates.ts +137 -0
  50. package/src/lib/delegate-client.test.ts +74 -0
  51. package/src/lib/delegate-client.ts +322 -0
  52. package/src/lib/ews-client.ts +3418 -0
  53. package/src/lib/git-commit.ts +4 -0
  54. package/src/lib/glitchtip-eligibility.ts +220 -0
  55. package/src/lib/glitchtip.ts +253 -0
  56. package/src/lib/global-env.ts +3 -0
  57. package/src/lib/graph-auth.ts +223 -0
  58. package/src/lib/graph-calendar-client.test.ts +118 -0
  59. package/src/lib/graph-calendar-client.ts +112 -0
  60. package/src/lib/graph-client.test.ts +107 -0
  61. package/src/lib/graph-client.ts +1058 -0
  62. package/src/lib/graph-constants.ts +12 -0
  63. package/src/lib/graph-directory.ts +116 -0
  64. package/src/lib/graph-event.ts +134 -0
  65. package/src/lib/graph-schedule.ts +173 -0
  66. package/src/lib/graph-subscriptions.ts +94 -0
  67. package/src/lib/graph-user-path.ts +13 -0
  68. package/src/lib/jwt-utils.ts +34 -0
  69. package/src/lib/markdown.test.ts +21 -0
  70. package/src/lib/markdown.ts +174 -0
  71. package/src/lib/mime-type.ts +106 -0
  72. package/src/lib/oof-client.test.ts +59 -0
  73. package/src/lib/oof-client.ts +122 -0
  74. package/src/lib/outlook-graph-client.test.ts +146 -0
  75. package/src/lib/outlook-graph-client.ts +649 -0
  76. package/src/lib/outlook-master-categories.ts +145 -0
  77. package/src/lib/package-info.ts +59 -0
  78. package/src/lib/places-client.ts +144 -0
  79. package/src/lib/planner-client.ts +1226 -0
  80. package/src/lib/rules-client.ts +178 -0
  81. package/src/lib/sharepoint-client.ts +101 -0
  82. package/src/lib/site-pages-client.ts +73 -0
  83. package/src/lib/todo-client.test.ts +298 -0
  84. package/src/lib/todo-client.ts +1309 -0
  85. package/src/lib/url-validation.ts +40 -0
  86. package/src/lib/utils.ts +45 -0
  87. package/src/lib/webhook-server.ts +51 -0
  88. package/src/test/auth.test.ts +104 -0
  89. package/src/test/cli.integration.test.ts +1083 -0
  90. package/src/test/ews-client.test.ts +268 -0
  91. package/src/test/mocks/index.ts +375 -0
  92. package/src/test/mocks/responses.ts +861 -0
@@ -0,0 +1,649 @@
1
+ import {
2
+ callGraph,
3
+ fetchAllPages,
4
+ fetchGraphRaw,
5
+ GraphApiError,
6
+ type GraphResponse,
7
+ graphError,
8
+ graphResult
9
+ } from './graph-client.js';
10
+ import { graphUserPath } from './graph-user-path.js';
11
+
12
+ function mailFoldersRoot(user?: string): string {
13
+ return graphUserPath(user, 'mailFolders');
14
+ }
15
+
16
+ function contactsRoot(user?: string): string {
17
+ return graphUserPath(user, 'contacts');
18
+ }
19
+
20
+ /** Graph [mailFolder](https://learn.microsoft.com/en-us/graph/api/resources/mailfolder) (subset). */
21
+ export interface OutlookMailFolder {
22
+ id: string;
23
+ displayName: string;
24
+ parentFolderId?: string;
25
+ childFolderCount?: number;
26
+ unreadItemCount?: number;
27
+ totalItemCount?: number;
28
+ }
29
+
30
+ /** Graph [message](https://learn.microsoft.com/en-us/graph/api/resources/message) (subset). */
31
+ export interface OutlookMessage {
32
+ id: string;
33
+ subject?: string;
34
+ bodyPreview?: string;
35
+ receivedDateTime?: string;
36
+ sentDateTime?: string;
37
+ isRead?: boolean;
38
+ importance?: string;
39
+ from?: { emailAddress?: { name?: string; address?: string } };
40
+ toRecipients?: Array<{ emailAddress?: { name?: string; address?: string } }>;
41
+ }
42
+
43
+ /** Graph [contact](https://learn.microsoft.com/en-us/graph/api/resources/contact) (subset). */
44
+ export interface OutlookContact {
45
+ id: string;
46
+ displayName?: string;
47
+ givenName?: string;
48
+ surname?: string;
49
+ emailAddresses?: Array<{ name?: string; address?: string; type?: string }>;
50
+ mobilePhone?: string;
51
+ businessPhones?: string[];
52
+ companyName?: string;
53
+ jobTitle?: string;
54
+ }
55
+
56
+ export async function listMailFolders(token: string, user?: string): Promise<GraphResponse<OutlookMailFolder[]>> {
57
+ return fetchAllPages<OutlookMailFolder>(token, mailFoldersRoot(user), 'Failed to list mail folders');
58
+ }
59
+
60
+ /** Child folders of a folder (e.g. under Inbox). */
61
+ export async function listChildMailFolders(
62
+ token: string,
63
+ parentFolderId: string,
64
+ user?: string
65
+ ): Promise<GraphResponse<OutlookMailFolder[]>> {
66
+ return fetchAllPages<OutlookMailFolder>(
67
+ token,
68
+ `${mailFoldersRoot(user)}/${encodeURIComponent(parentFolderId)}/childFolders`,
69
+ 'Failed to list child mail folders'
70
+ );
71
+ }
72
+
73
+ export async function getMailFolder(
74
+ token: string,
75
+ folderId: string,
76
+ user?: string
77
+ ): Promise<GraphResponse<OutlookMailFolder>> {
78
+ try {
79
+ const result = await callGraph<OutlookMailFolder>(
80
+ token,
81
+ `${mailFoldersRoot(user)}/${encodeURIComponent(folderId)}`
82
+ );
83
+ if (!result.ok || !result.data) {
84
+ return graphError(result.error?.message || 'Failed to get mail folder', result.error?.code, result.error?.status);
85
+ }
86
+ return graphResult(result.data);
87
+ } catch (err) {
88
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
89
+ return graphError(err instanceof Error ? err.message : 'Failed to get mail folder');
90
+ }
91
+ }
92
+
93
+ export async function createMailFolder(
94
+ token: string,
95
+ displayName: string,
96
+ parentFolderId: string | undefined,
97
+ user?: string
98
+ ): Promise<GraphResponse<OutlookMailFolder>> {
99
+ const body: Record<string, unknown> = { displayName };
100
+ if (parentFolderId) body.parentFolderId = parentFolderId;
101
+ try {
102
+ const result = await callGraph<OutlookMailFolder>(token, mailFoldersRoot(user), {
103
+ method: 'POST',
104
+ body: JSON.stringify(body)
105
+ });
106
+ if (!result.ok || !result.data) {
107
+ return graphError(
108
+ result.error?.message || 'Failed to create mail folder',
109
+ result.error?.code,
110
+ result.error?.status
111
+ );
112
+ }
113
+ return graphResult(result.data);
114
+ } catch (err) {
115
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
116
+ return graphError(err instanceof Error ? err.message : 'Failed to create mail folder');
117
+ }
118
+ }
119
+
120
+ export async function updateMailFolder(
121
+ token: string,
122
+ folderId: string,
123
+ displayName: string,
124
+ user?: string
125
+ ): Promise<GraphResponse<OutlookMailFolder>> {
126
+ try {
127
+ const result = await callGraph<OutlookMailFolder>(
128
+ token,
129
+ `${mailFoldersRoot(user)}/${encodeURIComponent(folderId)}`,
130
+ {
131
+ method: 'PATCH',
132
+ body: JSON.stringify({ displayName })
133
+ }
134
+ );
135
+ if (!result.ok || !result.data) {
136
+ return graphError(
137
+ result.error?.message || 'Failed to update mail folder',
138
+ result.error?.code,
139
+ result.error?.status
140
+ );
141
+ }
142
+ return graphResult(result.data);
143
+ } catch (err) {
144
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
145
+ return graphError(err instanceof Error ? err.message : 'Failed to update mail folder');
146
+ }
147
+ }
148
+
149
+ export async function deleteMailFolder(token: string, folderId: string, user?: string): Promise<GraphResponse<void>> {
150
+ try {
151
+ return await callGraph<void>(
152
+ token,
153
+ `${mailFoldersRoot(user)}/${encodeURIComponent(folderId)}`,
154
+ { method: 'DELETE' },
155
+ false
156
+ );
157
+ } catch (err) {
158
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
159
+ return graphError(err instanceof Error ? err.message : 'Failed to delete mail folder');
160
+ }
161
+ }
162
+
163
+ export interface MessagesQueryOptions {
164
+ filter?: string;
165
+ orderby?: string;
166
+ select?: string;
167
+ /** When set, only one page (no automatic paging). */
168
+ top?: number;
169
+ }
170
+
171
+ /** Query for `GET /me/messages` (mailbox-wide, not folder-scoped). */
172
+ export interface RootMailboxMessagesQuery extends MessagesQueryOptions {
173
+ skip?: number;
174
+ /**
175
+ * Sets OData `$search` (keyword search). Graph requires `ConsistencyLevel: eventual` on the request.
176
+ * Do not combine with `$filter` on the same request (Graph limitation).
177
+ */
178
+ search?: string;
179
+ }
180
+
181
+ /** Graph [attachment](https://learn.microsoft.com/en-us/graph/api/resources/attachment) on a message (subset). */
182
+ export interface GraphMailMessageAttachment {
183
+ id: string;
184
+ name?: string;
185
+ contentType?: string;
186
+ size?: number;
187
+ isInline?: boolean;
188
+ '@odata.type'?: string;
189
+ }
190
+
191
+ function messagesPath(folderId: string, user: string | undefined, query?: MessagesQueryOptions): string {
192
+ const params = new URLSearchParams();
193
+ if (query?.filter) params.set('$filter', query.filter);
194
+ if (query?.orderby) params.set('$orderby', query.orderby);
195
+ if (query?.select) params.set('$select', query.select);
196
+ if (query?.top !== undefined) params.set('$top', String(query.top));
197
+ const qs = params.toString() ? `?${params.toString()}` : '';
198
+ return `${mailFoldersRoot(user)}/${encodeURIComponent(folderId)}/messages${qs}`;
199
+ }
200
+
201
+ function mailboxMessagesPath(user: string | undefined, query?: RootMailboxMessagesQuery): string {
202
+ const params = new URLSearchParams();
203
+ if (query?.filter) params.set('$filter', query.filter);
204
+ if (query?.orderby) params.set('$orderby', query.orderby);
205
+ if (query?.select) params.set('$select', query.select);
206
+ if (query?.top !== undefined) params.set('$top', String(query.top));
207
+ if (query?.skip !== undefined) params.set('$skip', String(query.skip));
208
+ if (query?.search) {
209
+ const escaped = query.search.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
210
+ params.set('$search', `"${escaped}"`);
211
+ }
212
+ const qs = params.toString() ? `?${params.toString()}` : '';
213
+ return `${graphUserPath(user, 'messages')}${qs}`;
214
+ }
215
+
216
+ function searchRequestInit(query?: RootMailboxMessagesQuery): RequestInit | undefined {
217
+ return query?.search ? { headers: { ConsistencyLevel: 'eventual' } } : undefined;
218
+ }
219
+
220
+ /**
221
+ * List messages in a folder. Well-known folder ids include `inbox`, `sentitems`, `drafts`, `deleteditems`, `archive`, `junkemail`.
222
+ * Use `top` for a single page; omit `top` to page through all results (can be large).
223
+ */
224
+ export async function listMessagesInFolder(
225
+ token: string,
226
+ folderId: string,
227
+ user?: string,
228
+ query?: MessagesQueryOptions
229
+ ): Promise<GraphResponse<OutlookMessage[]>> {
230
+ const path = messagesPath(folderId, user, query);
231
+ const singlePage = query?.top !== undefined;
232
+
233
+ if (singlePage) {
234
+ try {
235
+ const result = await callGraph<{ value: OutlookMessage[] }>(token, path);
236
+ if (!result.ok || !result.data) {
237
+ return graphError(result.error?.message || 'Failed to list messages', result.error?.code, result.error?.status);
238
+ }
239
+ return graphResult(result.data.value || []);
240
+ } catch (err) {
241
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
242
+ return graphError(err instanceof Error ? err.message : 'Failed to list messages');
243
+ }
244
+ }
245
+
246
+ return fetchAllPages<OutlookMessage>(token, path, 'Failed to list messages');
247
+ }
248
+
249
+ /**
250
+ * List messages via `GET /me/messages` (entire mailbox, not limited to one folder).
251
+ * When `search` is set, Graph requires `ConsistencyLevel: eventual` (applied automatically).
252
+ */
253
+ export async function listMailboxMessages(
254
+ token: string,
255
+ user?: string,
256
+ query?: RootMailboxMessagesQuery
257
+ ): Promise<GraphResponse<OutlookMessage[]>> {
258
+ const path = mailboxMessagesPath(user, query);
259
+ const singlePage = query?.top !== undefined;
260
+ const req = searchRequestInit(query);
261
+
262
+ if (singlePage) {
263
+ try {
264
+ const result = await callGraph<{ value: OutlookMessage[] }>(token, path, req ?? {});
265
+ if (!result.ok || !result.data) {
266
+ return graphError(result.error?.message || 'Failed to list messages', result.error?.code, result.error?.status);
267
+ }
268
+ return graphResult(result.data.value || []);
269
+ } catch (err) {
270
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
271
+ return graphError(err instanceof Error ? err.message : 'Failed to list messages');
272
+ }
273
+ }
274
+
275
+ return fetchAllPages<OutlookMessage>(token, path, 'Failed to list messages', undefined, req);
276
+ }
277
+
278
+ /** `GET /me/messages/{id}` — message ids are unique within the mailbox (folder path not required). */
279
+ export async function getMessage(
280
+ token: string,
281
+ messageId: string,
282
+ user?: string,
283
+ select?: string
284
+ ): Promise<GraphResponse<OutlookMessage>> {
285
+ const qs = select ? `?$select=${encodeURIComponent(select)}` : '';
286
+ try {
287
+ const result = await callGraph<OutlookMessage>(
288
+ token,
289
+ `${graphUserPath(user, 'messages')}/${encodeURIComponent(messageId)}${qs}`
290
+ );
291
+ if (!result.ok || !result.data) {
292
+ return graphError(result.error?.message || 'Failed to get message', result.error?.code, result.error?.status);
293
+ }
294
+ return graphResult(result.data);
295
+ } catch (err) {
296
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
297
+ return graphError(err instanceof Error ? err.message : 'Failed to get message');
298
+ }
299
+ }
300
+
301
+ export async function patchMailMessage(
302
+ token: string,
303
+ messageId: string,
304
+ patch: Record<string, unknown>,
305
+ user?: string
306
+ ): Promise<GraphResponse<OutlookMessage>> {
307
+ try {
308
+ const result = await callGraph<OutlookMessage>(
309
+ token,
310
+ `${graphUserPath(user, 'messages')}/${encodeURIComponent(messageId)}`,
311
+ { method: 'PATCH', body: JSON.stringify(patch) }
312
+ );
313
+ if (!result.ok || !result.data) {
314
+ return graphError(result.error?.message || 'Failed to update message', result.error?.code, result.error?.status);
315
+ }
316
+ return graphResult(result.data);
317
+ } catch (err) {
318
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
319
+ return graphError(err instanceof Error ? err.message : 'Failed to update message');
320
+ }
321
+ }
322
+
323
+ export async function deleteMailMessage(token: string, messageId: string, user?: string): Promise<GraphResponse<void>> {
324
+ try {
325
+ return await callGraph<void>(
326
+ token,
327
+ `${graphUserPath(user, 'messages')}/${encodeURIComponent(messageId)}`,
328
+ { method: 'DELETE' },
329
+ false
330
+ );
331
+ } catch (err) {
332
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
333
+ return graphError(err instanceof Error ? err.message : 'Failed to delete message');
334
+ }
335
+ }
336
+
337
+ export async function moveMailMessage(
338
+ token: string,
339
+ messageId: string,
340
+ destinationFolderId: string,
341
+ user?: string
342
+ ): Promise<GraphResponse<OutlookMessage>> {
343
+ try {
344
+ const result = await callGraph<OutlookMessage>(
345
+ token,
346
+ `${graphUserPath(user, 'messages')}/${encodeURIComponent(messageId)}/move`,
347
+ {
348
+ method: 'POST',
349
+ body: JSON.stringify({ destinationId: destinationFolderId })
350
+ }
351
+ );
352
+ if (!result.ok || !result.data) {
353
+ return graphError(result.error?.message || 'Failed to move message', result.error?.code, result.error?.status);
354
+ }
355
+ return graphResult(result.data);
356
+ } catch (err) {
357
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
358
+ return graphError(err instanceof Error ? err.message : 'Failed to move message');
359
+ }
360
+ }
361
+
362
+ export async function copyMailMessage(
363
+ token: string,
364
+ messageId: string,
365
+ destinationFolderId: string,
366
+ user?: string
367
+ ): Promise<GraphResponse<OutlookMessage>> {
368
+ try {
369
+ const result = await callGraph<OutlookMessage>(
370
+ token,
371
+ `${graphUserPath(user, 'messages')}/${encodeURIComponent(messageId)}/copy`,
372
+ {
373
+ method: 'POST',
374
+ body: JSON.stringify({ destinationId: destinationFolderId })
375
+ }
376
+ );
377
+ if (!result.ok || !result.data) {
378
+ return graphError(result.error?.message || 'Failed to copy message', result.error?.code, result.error?.status);
379
+ }
380
+ return graphResult(result.data);
381
+ } catch (err) {
382
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
383
+ return graphError(err instanceof Error ? err.message : 'Failed to copy message');
384
+ }
385
+ }
386
+
387
+ /** Send a new message in one request (`POST /sendMail`). */
388
+ export async function sendMail(
389
+ token: string,
390
+ body: { message: Record<string, unknown>; saveToSentItems?: boolean },
391
+ user?: string
392
+ ): Promise<GraphResponse<void>> {
393
+ try {
394
+ return await callGraph<void>(
395
+ token,
396
+ `${graphUserPath(user, 'sendMail')}`,
397
+ { method: 'POST', body: JSON.stringify(body) },
398
+ false
399
+ );
400
+ } catch (err) {
401
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
402
+ return graphError(err instanceof Error ? err.message : 'Failed to send mail');
403
+ }
404
+ }
405
+
406
+ /** Send an existing draft (`POST /messages/{id}/send`). */
407
+ export async function sendMailMessage(token: string, messageId: string, user?: string): Promise<GraphResponse<void>> {
408
+ try {
409
+ return await callGraph<void>(
410
+ token,
411
+ `${graphUserPath(user, 'messages')}/${encodeURIComponent(messageId)}/send`,
412
+ { method: 'POST' },
413
+ false
414
+ );
415
+ } catch (err) {
416
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
417
+ return graphError(err instanceof Error ? err.message : 'Failed to send message');
418
+ }
419
+ }
420
+
421
+ export async function listMailMessageAttachments(
422
+ token: string,
423
+ messageId: string,
424
+ user?: string
425
+ ): Promise<GraphResponse<GraphMailMessageAttachment[]>> {
426
+ return fetchAllPages<GraphMailMessageAttachment>(
427
+ token,
428
+ `${graphUserPath(user, 'messages')}/${encodeURIComponent(messageId)}/attachments`,
429
+ 'Failed to list attachments'
430
+ );
431
+ }
432
+
433
+ export async function getMailMessageAttachment(
434
+ token: string,
435
+ messageId: string,
436
+ attachmentId: string,
437
+ user?: string
438
+ ): Promise<GraphResponse<GraphMailMessageAttachment>> {
439
+ try {
440
+ const result = await callGraph<GraphMailMessageAttachment>(
441
+ token,
442
+ `${graphUserPath(user, 'messages')}/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}`
443
+ );
444
+ if (!result.ok || !result.data) {
445
+ return graphError(result.error?.message || 'Failed to get attachment', result.error?.code, result.error?.status);
446
+ }
447
+ return graphResult(result.data);
448
+ } catch (err) {
449
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
450
+ return graphError(err instanceof Error ? err.message : 'Failed to get attachment');
451
+ }
452
+ }
453
+
454
+ export async function downloadMailMessageAttachmentBytes(
455
+ token: string,
456
+ messageId: string,
457
+ attachmentId: string,
458
+ user?: string
459
+ ): Promise<GraphResponse<Uint8Array>> {
460
+ const path = `${graphUserPath(user, 'messages')}/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}/$value`;
461
+ try {
462
+ const res = await fetchGraphRaw(token, path);
463
+ if (!res.ok) {
464
+ let message = `Failed to download attachment: HTTP ${res.status}`;
465
+ try {
466
+ const json = (await res.json()) as { error?: { message?: string } };
467
+ message = json.error?.message || message;
468
+ } catch {
469
+ // ignore
470
+ }
471
+ return graphError(message, undefined, res.status);
472
+ }
473
+ const buf = new Uint8Array(await res.arrayBuffer());
474
+ return graphResult(buf);
475
+ } catch (err) {
476
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
477
+ return graphError(err instanceof Error ? err.message : 'Failed to download attachment');
478
+ }
479
+ }
480
+
481
+ export async function createMailReplyDraft(
482
+ token: string,
483
+ messageId: string,
484
+ user?: string,
485
+ comment?: string
486
+ ): Promise<GraphResponse<OutlookMessage>> {
487
+ const body = JSON.stringify(comment ? { comment } : {});
488
+ try {
489
+ const result = await callGraph<OutlookMessage>(
490
+ token,
491
+ `${graphUserPath(user, 'messages')}/${encodeURIComponent(messageId)}/createReply`,
492
+ { method: 'POST', body },
493
+ true
494
+ );
495
+ if (!result.ok || !result.data) {
496
+ return graphError(
497
+ result.error?.message || 'Failed to create reply draft',
498
+ result.error?.code,
499
+ result.error?.status
500
+ );
501
+ }
502
+ return graphResult(result.data);
503
+ } catch (err) {
504
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
505
+ return graphError(err instanceof Error ? err.message : 'Failed to create reply draft');
506
+ }
507
+ }
508
+
509
+ export async function createMailReplyAllDraft(
510
+ token: string,
511
+ messageId: string,
512
+ user?: string,
513
+ comment?: string
514
+ ): Promise<GraphResponse<OutlookMessage>> {
515
+ const body = JSON.stringify(comment ? { comment } : {});
516
+ try {
517
+ const result = await callGraph<OutlookMessage>(
518
+ token,
519
+ `${graphUserPath(user, 'messages')}/${encodeURIComponent(messageId)}/createReplyAll`,
520
+ { method: 'POST', body },
521
+ true
522
+ );
523
+ if (!result.ok || !result.data) {
524
+ return graphError(
525
+ result.error?.message || 'Failed to create reply-all draft',
526
+ result.error?.code,
527
+ result.error?.status
528
+ );
529
+ }
530
+ return graphResult(result.data);
531
+ } catch (err) {
532
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
533
+ return graphError(err instanceof Error ? err.message : 'Failed to create reply-all draft');
534
+ }
535
+ }
536
+
537
+ export async function createMailForwardDraft(
538
+ token: string,
539
+ messageId: string,
540
+ toRecipients: string[],
541
+ user?: string,
542
+ comment?: string
543
+ ): Promise<GraphResponse<OutlookMessage>> {
544
+ const recipients = toRecipients.map((address) => ({
545
+ emailAddress: { address }
546
+ }));
547
+ const payload: Record<string, unknown> = { toRecipients: recipients };
548
+ if (comment) payload.comment = comment;
549
+ try {
550
+ const result = await callGraph<OutlookMessage>(
551
+ token,
552
+ `${graphUserPath(user, 'messages')}/${encodeURIComponent(messageId)}/createForward`,
553
+ { method: 'POST', body: JSON.stringify(payload) },
554
+ true
555
+ );
556
+ if (!result.ok || !result.data) {
557
+ return graphError(
558
+ result.error?.message || 'Failed to create forward draft',
559
+ result.error?.code,
560
+ result.error?.status
561
+ );
562
+ }
563
+ return graphResult(result.data);
564
+ } catch (err) {
565
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
566
+ return graphError(err instanceof Error ? err.message : 'Failed to create forward draft');
567
+ }
568
+ }
569
+
570
+ export async function listContacts(token: string, user?: string): Promise<GraphResponse<OutlookContact[]>> {
571
+ return fetchAllPages<OutlookContact>(token, contactsRoot(user), 'Failed to list contacts');
572
+ }
573
+
574
+ export async function getContact(
575
+ token: string,
576
+ contactId: string,
577
+ user?: string,
578
+ select?: string
579
+ ): Promise<GraphResponse<OutlookContact>> {
580
+ const qs = select ? `?$select=${encodeURIComponent(select)}` : '';
581
+ try {
582
+ const result = await callGraph<OutlookContact>(
583
+ token,
584
+ `${contactsRoot(user)}/${encodeURIComponent(contactId)}${qs}`
585
+ );
586
+ if (!result.ok || !result.data) {
587
+ return graphError(result.error?.message || 'Failed to get contact', result.error?.code, result.error?.status);
588
+ }
589
+ return graphResult(result.data);
590
+ } catch (err) {
591
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
592
+ return graphError(err instanceof Error ? err.message : 'Failed to get contact');
593
+ }
594
+ }
595
+
596
+ export async function createContact(
597
+ token: string,
598
+ body: Record<string, unknown>,
599
+ user?: string
600
+ ): Promise<GraphResponse<OutlookContact>> {
601
+ try {
602
+ const result = await callGraph<OutlookContact>(token, contactsRoot(user), {
603
+ method: 'POST',
604
+ body: JSON.stringify(body)
605
+ });
606
+ if (!result.ok || !result.data) {
607
+ return graphError(result.error?.message || 'Failed to create contact', result.error?.code, result.error?.status);
608
+ }
609
+ return graphResult(result.data);
610
+ } catch (err) {
611
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
612
+ return graphError(err instanceof Error ? err.message : 'Failed to create contact');
613
+ }
614
+ }
615
+
616
+ export async function updateContact(
617
+ token: string,
618
+ contactId: string,
619
+ patch: Record<string, unknown>,
620
+ user?: string
621
+ ): Promise<GraphResponse<OutlookContact>> {
622
+ try {
623
+ const result = await callGraph<OutlookContact>(token, `${contactsRoot(user)}/${encodeURIComponent(contactId)}`, {
624
+ method: 'PATCH',
625
+ body: JSON.stringify(patch)
626
+ });
627
+ if (!result.ok || !result.data) {
628
+ return graphError(result.error?.message || 'Failed to update contact', result.error?.code, result.error?.status);
629
+ }
630
+ return graphResult(result.data);
631
+ } catch (err) {
632
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
633
+ return graphError(err instanceof Error ? err.message : 'Failed to update contact');
634
+ }
635
+ }
636
+
637
+ export async function deleteContact(token: string, contactId: string, user?: string): Promise<GraphResponse<void>> {
638
+ try {
639
+ return await callGraph<void>(
640
+ token,
641
+ `${contactsRoot(user)}/${encodeURIComponent(contactId)}`,
642
+ { method: 'DELETE' },
643
+ false
644
+ );
645
+ } catch (err) {
646
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
647
+ return graphError(err instanceof Error ? err.message : 'Failed to delete contact');
648
+ }
649
+ }