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,861 @@
1
+ /**
2
+ * Mock API responses for CLI integration testing.
3
+ * These mirror the exact XML/JSON structures that the EWS and Graph clients parse.
4
+ */
5
+
6
+ const SOAP_NS =
7
+ 'xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"';
8
+
9
+ function soapResponse(body: string): string {
10
+ return `<?xml version="1.0" encoding="utf-8"?>
11
+ <soap:Envelope ${SOAP_NS}>
12
+ <soap:Header><t:RequestServerVersion Version="Exchange2016" /></soap:Header>
13
+ <soap:Body>
14
+ <m:ResponseCode>NoError</m:ResponseCode>
15
+ ${body}
16
+ </soap:Body>
17
+ </soap:Envelope>`;
18
+ }
19
+
20
+ // ─── OAuth ────────────────────────────────────────────────────────────────
21
+
22
+ export const mockOAuthTokenResponse = JSON.stringify({
23
+ access_token: 'mock-access-token-12345',
24
+ refresh_token: 'mock-refresh-token-12345',
25
+ expires_in: 3600,
26
+ token_type: 'Bearer'
27
+ });
28
+
29
+ // ─── whoami ───────────────────────────────────────────────────────────────
30
+
31
+ export const mockResolveNamesResponse = soapResponse(`
32
+ <m:ResolutionSet>
33
+ <m:Resolution>
34
+ <t:Mailbox>
35
+ <t:Name>Test User</t:Name>
36
+ <t:EmailAddress>test@example.com</t:EmailAddress>
37
+ <t:RoutingType>SMTP</t:RoutingType>
38
+ <t:MailboxType>Mailbox</t:MailboxType>
39
+ </t:Mailbox>
40
+ </m:Resolution>
41
+ </m:ResolutionSet>
42
+ `);
43
+
44
+ // ─── calendar ─────────────────────────────────────────────────────────────
45
+
46
+ function makeCalendarItem(opts: {
47
+ id: string;
48
+ changeKey?: string;
49
+ subject: string;
50
+ start: string;
51
+ end: string;
52
+ location?: string;
53
+ isAllDay?: boolean;
54
+ isCancelled?: boolean;
55
+ isOrganizer?: boolean;
56
+ organizerName?: string;
57
+ organizerEmail?: string;
58
+ myResponseType?: string;
59
+ attendees?: string;
60
+ categories?: string;
61
+ }): string {
62
+ const ck = opts.changeKey ?? 'mockChangeKey1';
63
+ const loc = opts.location ? `<t:Location>${opts.location}</t:Location>` : '';
64
+ const allDay = opts.isAllDay ? '<t:IsAllDayEvent>true</t:IsAllDayEvent>' : '';
65
+ const canc = opts.isCancelled ? '<t:IsCancelled>true</t:IsCancelled>' : '';
66
+ const orgBlock = opts.organizerName
67
+ ? `<t:Organizer><t:Mailbox><t:Name>${opts.organizerName}</t:Name><t:EmailAddress>${opts.organizerEmail ?? 'test@example.com'}</t:EmailAddress></t:Mailbox></t:Organizer>`
68
+ : '';
69
+ const respType = opts.myResponseType ? `<t:MyResponseType>${opts.myResponseType}</t:MyResponseType>` : '';
70
+ const cats = opts.categories ? `<t:Categories>${opts.categories}</t:Categories>` : '';
71
+ return `
72
+ <t:CalendarItem>
73
+ <t:ItemId Id="${opts.id}" ChangeKey="${ck}" />
74
+ <t:Subject>${opts.subject}</t:Subject>
75
+ <t:Start>${opts.start}</t:Start>
76
+ <t:End>${opts.end}</t:End>
77
+ ${loc}
78
+ ${allDay}
79
+ ${canc}
80
+ ${orgBlock}
81
+ ${respType}
82
+ <t:Importance>Normal</t:Importance>
83
+ <t:LegacyFreeBusyStatus>Busy</t:LegacyFreeBusyStatus>
84
+ ${opts.attendees ?? ''}
85
+ ${cats}
86
+ <t:TextBody BodyType="Text">Meeting description</t:TextBody>
87
+ </t:CalendarItem>`;
88
+ }
89
+
90
+ /** GetItem response for calendar event IDs (respond/cancel/delete prefetch). */
91
+ export function makeGetCalendarItemDetailResponse(itemId: string): string {
92
+ return soapResponse(`
93
+ <m:GetItemResponse>
94
+ <m:ResponseCode>NoError</m:ResponseCode>
95
+ <m:Items>
96
+ ${makeCalendarItem({
97
+ id: itemId,
98
+ subject: 'Calendar item',
99
+ start: '2026-03-30T10:00:00Z',
100
+ end: '2026-03-30T11:00:00Z',
101
+ isOrganizer: true,
102
+ myResponseType: 'Organizer'
103
+ })}
104
+ </m:Items>
105
+ </m:GetItemResponse>
106
+ `);
107
+ }
108
+
109
+ const MOCK_CALENDAR_RESPONSE = soapResponse(`
110
+ <m:FindItemResponse>
111
+ <m:ResponseCode>NoError</m:ResponseCode>
112
+ <m:RootFolder>
113
+ <t:TotalItemsInView>2</t:TotalItemsInView>
114
+ <m:Items>
115
+ ${makeCalendarItem({
116
+ id: 'event-1',
117
+ subject: 'Team Standup',
118
+ start: '2026-03-30T09:00:00Z',
119
+ end: '2026-03-30T09:30:00Z',
120
+ location: 'Conference Room A',
121
+ isOrganizer: true,
122
+ myResponseType: 'Organizer',
123
+ attendees: `
124
+ <t:RequiredAttendees>
125
+ <t:Attendee><t:Mailbox><t:Name>Alice</t:Name><t:EmailAddress>alice@example.com</t:EmailAddress></t:Mailbox><t:ResponseType>Accept</t:ResponseType></t:Attendee>
126
+ <t:Attendee><t:Mailbox><t:Name>Bob</t:Name><t:EmailAddress>bob@example.com</t:EmailAddress></t:Mailbox><t:ResponseType>NoResponseReceived</t:ResponseType></t:Attendee>
127
+ </t:RequiredAttendees>`
128
+ })}
129
+ ${makeCalendarItem({
130
+ id: 'event-2',
131
+ subject: 'Project Review',
132
+ start: '2026-03-30T14:00:00Z',
133
+ end: '2026-03-30T15:00:00Z',
134
+ isOrganizer: true,
135
+ myResponseType: 'Organizer',
136
+ categories: '<t:String>Work</t:String><t:String>Important</t:String>'
137
+ })}
138
+ </m:Items>
139
+ </m:RootFolder>
140
+ </m:FindItemResponse>
141
+ `);
142
+
143
+ export const mockCalendarEventsResponse = MOCK_CALENDAR_RESPONSE;
144
+
145
+ export const mockCalendarEventsEmptyResponse = soapResponse(`
146
+ <m:FindItemResponse>
147
+ <m:ResponseCode>NoError</m:ResponseCode>
148
+ <m:RootFolder>
149
+ <t:TotalItemsInView>0</t:TotalItemsInView>
150
+ <m:Items />
151
+ </m:RootFolder>
152
+ </m:FindItemResponse>
153
+ `);
154
+
155
+ export const mockCalendarEventsWithCancelled = soapResponse(`
156
+ <m:FindItemResponse>
157
+ <m:ResponseCode>NoError</m:ResponseCode>
158
+ <m:RootFolder>
159
+ <t:TotalItemsInView>2</t:TotalItemsInView>
160
+ <m:Items>
161
+ ${makeCalendarItem({
162
+ id: 'event-1',
163
+ subject: 'Cancelled Meeting',
164
+ start: '2026-03-30T10:00:00Z',
165
+ end: '2026-03-30T11:00:00Z',
166
+ isCancelled: true,
167
+ isOrganizer: true,
168
+ myResponseType: 'Organizer'
169
+ })}
170
+ ${makeCalendarItem({
171
+ id: 'event-2',
172
+ subject: 'Valid Meeting',
173
+ start: '2026-03-30T11:00:00Z',
174
+ end: '2026-03-30T12:00:00Z',
175
+ isOrganizer: true,
176
+ myResponseType: 'Organizer'
177
+ })}
178
+ </m:Items>
179
+ </m:RootFolder>
180
+ </m:FindItemResponse>
181
+ `);
182
+
183
+ // ─── findtime ─────────────────────────────────────────────────────────────
184
+
185
+ export const mockGetScheduleResponse = soapResponse(`
186
+ <m:GetScheduleResponse>
187
+ <m:ResponseCode>NoError</m:ResponseCode>
188
+ <m:ScheduleInfo>
189
+ <t:ScheduleResourceEmailAddress>test@example.com</t:ScheduleResourceEmailAddress>
190
+ <t:ScheduleItem>
191
+ <t:Start>2026-03-30T09:00:00Z</t:Start>
192
+ <t:End>2026-03-30T10:00:00Z</t:End>
193
+ <t:Status>Free</t:Status>
194
+ <t:IsPrivate>false</t:IsPrivate>
195
+ <t:IsMeeting>true</t:IsMeeting>
196
+ <t:IsRecurring>false</t:IsRecurring>
197
+ <t:IsException>false</t:IsException>
198
+ <t:MeetingTimeZone>UTC</t:MeetingTimeZone>
199
+ </t:ScheduleItem>
200
+ <t:ScheduleItem>
201
+ <t:Start>2026-03-30T11:00:00Z</t:Start>
202
+ <t:End>2026-03-30T12:00:00Z</t:End>
203
+ <t:Status>Busy</t:Status>
204
+ <t:IsPrivate>false</t:IsPrivate>
205
+ <t:IsMeeting>true</t:IsMeeting>
206
+ <t:IsRecurring>false</t:IsRecurring>
207
+ <t:IsException>false</t:IsException>
208
+ <t:MeetingTimeZone>UTC</t:MeetingTimeZone>
209
+ </t:ScheduleItem>
210
+ <t:ScheduleItem>
211
+ <t:Start>2026-03-30T14:00:00Z</t:Start>
212
+ <t:End>2026-03-30T15:00:00Z</t:End>
213
+ <t:Status>Free</t:Status>
214
+ <t:IsPrivate>false</t:IsPrivate>
215
+ <t:IsMeeting>true</t:IsMeeting>
216
+ <t:IsRecurring>false</t:IsRecurring>
217
+ <t:IsException>false</t:IsException>
218
+ <t:MeetingTimeZone>UTC</t:MeetingTimeZone>
219
+ </t:ScheduleItem>
220
+ </t:ScheduleInfo>
221
+ </m:GetScheduleResponse>
222
+ `);
223
+
224
+ export const mockGetScheduleEmptyResponse = soapResponse(`
225
+ <m:GetScheduleResponse>
226
+ <m:ResponseCode>NoError</m:ResponseCode>
227
+ <m:ScheduleInfo>
228
+ <t:ScheduleResourceEmailAddress>test@example.com</t:ScheduleResourceEmailAddress>
229
+ <t:ScheduleItem>
230
+ <t:Start>2026-03-30T09:00:00Z</t:Start>
231
+ <t:End>2026-03-30T17:00:00Z</t:End>
232
+ <t:Status>Busy</t:Status>
233
+ <t:IsPrivate>false</t:IsPrivate>
234
+ <t:IsMeeting>true</t:IsMeeting>
235
+ <t:IsRecurring>false</t:IsRecurring>
236
+ <t:IsException>false</t:IsException>
237
+ <t:MeetingTimeZone>UTC</t:MeetingTimeZone>
238
+ </t:ScheduleItem>
239
+ </t:ScheduleInfo>
240
+ </m:GetScheduleResponse>
241
+ `);
242
+
243
+ // ─── respond (pending invitations) ─────────────────────────────────────────
244
+
245
+ export const mockRespondListResponse = soapResponse(`
246
+ <m:FindItemResponse>
247
+ <m:ResponseCode>NoError</m:ResponseCode>
248
+ <m:RootFolder>
249
+ <t:TotalItemsInView>2</t:TotalItemsInView>
250
+ <m:Items>
251
+ ${makeCalendarItem({
252
+ id: 'invite-1',
253
+ subject: 'Invited Meeting 1',
254
+ start: '2026-03-31T10:00:00Z',
255
+ end: '2026-03-31T11:00:00Z',
256
+ location: 'Room ABC',
257
+ isOrganizer: false,
258
+ organizerName: 'Organizer Person',
259
+ organizerEmail: 'organizer@example.com',
260
+ attendees: `
261
+ <t:RequiredAttendees>
262
+ <t:Attendee><t:Mailbox><t:Name>Test User</t:Name><t:EmailAddress>test@example.com</t:EmailAddress></t:Mailbox><t:ResponseType>NoResponseReceived</t:ResponseType></t:Attendee>
263
+ </t:RequiredAttendees>`
264
+ })}
265
+ ${makeCalendarItem({
266
+ id: 'invite-2',
267
+ subject: 'Invited Meeting 2',
268
+ start: '2026-04-01T14:00:00Z',
269
+ end: '2026-04-01T15:30:00Z',
270
+ isOrganizer: false,
271
+ organizerName: 'Another Organizer',
272
+ organizerEmail: 'another@example.com'
273
+ })}
274
+ </m:Items>
275
+ </m:RootFolder>
276
+ </m:FindItemResponse>
277
+ `);
278
+
279
+ export const mockRespondSuccessResponse = soapResponse(`
280
+ <m:RespondToItemResponse>
281
+ <m:ResponseCode>NoError</m:ResponseCode>
282
+ </m:RespondToItemResponse>
283
+ `);
284
+
285
+ // ─── create-event ─────────────────────────────────────────────────────────
286
+
287
+ export const mockCreateEventResponse = soapResponse(`
288
+ <m:CreateItemResponse>
289
+ <m:ResponseCode>NoError</m:ResponseCode>
290
+ <m:Items>
291
+ <t:CalendarItem>
292
+ <t:ItemId Id="new-event-id-123" ChangeKey="newChangeKey456" />
293
+ <t:Subject>New Meeting</t:Subject>
294
+ <t:Start>2026-03-30T10:00:00Z</t:Start>
295
+ <t:End>2026-03-30T11:00:00Z</t:End>
296
+ <t:Location><t:DisplayName>Conference Room A</t:DisplayName></t:Location>
297
+ <t:WebLink>https://outlook.office365.com/owa/item?itemId=abc</t:WebLink>
298
+ </t:CalendarItem>
299
+ </m:Items>
300
+ </m:CreateItemResponse>
301
+ `);
302
+
303
+ // ─── delete-event ──────────────────────────────────────────────────────────
304
+
305
+ export const mockDeleteEventSuccessResponse = soapResponse(`
306
+ <m:DeleteItemResponse>
307
+ <m:ResponseCode>NoError</m:ResponseCode>
308
+ </m:DeleteItemResponse>
309
+ `);
310
+
311
+ export const mockCancelEventSuccessResponse = soapResponse(`
312
+ <m:CancelItemResponse>
313
+ <m:ResponseCode>NoError</m:ResponseCode>
314
+ </m:CancelItemResponse>
315
+ `);
316
+
317
+ // ─── find (resolveNames) ──────────────────────────────────────────────────
318
+
319
+ export const mockResolveNamesPeopleResponse = soapResponse(`
320
+ <m:ResolveNamesResponse>
321
+ <m:ResponseCode>NoError</m:ResponseCode>
322
+ <m:ResolutionSet>
323
+ <m:Resolution>
324
+ <t:Mailbox>
325
+ <t:Name>John Doe</t:Name>
326
+ <t:EmailAddress>john.doe@example.com</t:EmailAddress>
327
+ <t:RoutingType>SMTP</t:RoutingType>
328
+ <t:MailboxType>Mailbox</t:MailboxType>
329
+ </t:Mailbox>
330
+ <t:Contact>
331
+ <t:DisplayName>John Doe</t:DisplayName>
332
+ <t:EmailAddresses><t:Entry Key="EmailAddress1">SMTP:john.doe@example.com</t:Entry></t:EmailAddresses>
333
+ <t:JobTitle>Software Engineer</t:JobTitle>
334
+ <t:Department>Engineering</t:Department>
335
+ </t:Contact>
336
+ </m:Resolution>
337
+ <m:Resolution>
338
+ <t:Mailbox>
339
+ <t:Name>Jane Smith</t:Name>
340
+ <t:EmailAddress>jane.smith@example.com</t:EmailAddress>
341
+ <t:RoutingType>SMTP</t:RoutingType>
342
+ <t:MailboxType>Mailbox</t:MailboxType>
343
+ </t:Mailbox>
344
+ <t:Contact>
345
+ <t:DisplayName>Jane Smith</t:DisplayName>
346
+ <t:EmailAddresses><t:Entry Key="EmailAddress1">SMTP:jane.smith@example.com</t:Entry></t:EmailAddresses>
347
+ <t:JobTitle>Product Manager</t:JobTitle>
348
+ <t:Department>Product</t:Department>
349
+ </t:Contact>
350
+ </m:Resolution>
351
+ </m:ResolutionSet>
352
+ </m:ResolveNamesResponse>
353
+ `);
354
+
355
+ export const mockResolveNamesRoomsResponse = soapResponse(`
356
+ <m:ResolveNamesResponse>
357
+ <m:ResponseCode>NoError</m:ResponseCode>
358
+ <m:ResolutionSet>
359
+ <m:Resolution>
360
+ <t:Mailbox>
361
+ <t:Name>Conference Room Alpha</t:Name>
362
+ <t:EmailAddress>conf-alpha@example.com</t:EmailAddress>
363
+ <t:RoutingType>SMTP</t:RoutingType>
364
+ <t:MailboxType>Room</t:MailboxType>
365
+ </t:Mailbox>
366
+ </m:Resolution>
367
+ <m:Resolution>
368
+ <t:Mailbox>
369
+ <t:Name>Conference Room Beta</t:Name>
370
+ <t:EmailAddress>conf-beta@example.com</t:EmailAddress>
371
+ <t:RoutingType>SMTP</t:RoutingType>
372
+ <t:MailboxType>Room</t:MailboxType>
373
+ </t:Mailbox>
374
+ </m:Resolution>
375
+ </m:ResolutionSet>
376
+ </m:ResolveNamesResponse>
377
+ `);
378
+
379
+ export const mockResolveNamesEmptyResponse = soapResponse(`
380
+ <m:ResolveNamesResponse>
381
+ <m:ResponseCode>NoError</m:ResponseCode>
382
+ <m:ResolutionSet TotalItemsInView="0" IncludesLastItemInRange="true" />
383
+ </m:ResolveNamesResponse>
384
+ `);
385
+
386
+ // ─── update-event ─────────────────────────────────────────────────────────
387
+
388
+ export const mockUpdateEventResponse = soapResponse(`
389
+ <m:UpdateItemResponse>
390
+ <m:ResponseCode>NoError</m:ResponseCode>
391
+ <m:Items>
392
+ <t:CalendarItem>
393
+ <t:ItemId Id="event-update-1" ChangeKey="updatedChangeKey789" />
394
+ <t:Subject>Updated Meeting Title</t:Subject>
395
+ <t:Start>2026-03-30T10:00:00Z</t:Start>
396
+ <t:End>2026-03-30T11:00:00Z</t:End>
397
+ </t:CalendarItem>
398
+ </m:Items>
399
+ </m:UpdateItemResponse>
400
+ `);
401
+
402
+ // ─── mail ─────────────────────────────────────────────────────────────────
403
+
404
+ function makeEmailItem(opts: {
405
+ id: string;
406
+ subject: string;
407
+ fromName?: string;
408
+ fromEmail?: string;
409
+ to?: string;
410
+ cc?: string;
411
+ body?: string;
412
+ receivedDateTime?: string;
413
+ isRead?: boolean;
414
+ hasAttachments?: boolean;
415
+ importance?: string;
416
+ flagStatus?: string;
417
+ }): string {
418
+ const from = opts.fromName
419
+ ? `<t:From><t:Mailbox><t:Name>${opts.fromName}</t:Name><t:EmailAddress>${opts.fromEmail ?? 'sender@example.com'}</t:EmailAddress></t:Mailbox></t:From>`
420
+ : '';
421
+ const toList = opts.to
422
+ ? `<t:ToRecipients>${opts.to
423
+ .split(',')
424
+ .map((e) => `<t:Mailbox><t:EmailAddress>${e.trim()}</t:EmailAddress></t:Mailbox>`)
425
+ .join('')}</t:ToRecipients>`
426
+ : '';
427
+ const ccList = opts.cc
428
+ ? `<t:CcRecipients>${opts.cc
429
+ .split(',')
430
+ .map((e) => `<t:Mailbox><t:EmailAddress>${e.trim()}</t:EmailAddress></t:Mailbox>`)
431
+ .join('')}</t:CcRecipients>`
432
+ : '';
433
+ const body = opts.body ? `<t:Body BodyType="Text">${opts.body}</t:Body>` : '';
434
+ const flag = opts.flagStatus ? `<t:Flag><t:FlagStatus>${opts.flagStatus}</t:FlagStatus></t:Flag>` : '';
435
+ return `
436
+ <t:Message>
437
+ <t:ItemId Id="${opts.id}" ChangeKey="emailChangeKey1" />
438
+ ${from}
439
+ ${toList}
440
+ ${ccList}
441
+ <t:Subject>${opts.subject}</t:Subject>
442
+ ${body}
443
+ <t:DateTimeReceived>${opts.receivedDateTime ?? '2026-03-30T09:00:00Z'}</t:DateTimeReceived>
444
+ <t:IsRead>${(opts.isRead ?? false) ? 'true' : 'false'}</t:IsRead>
445
+ <t:HasAttachments>${(opts.hasAttachments ?? false) ? 'true' : 'false'}</t:HasAttachments>
446
+ <t:Importance>${opts.importance ?? 'Normal'}</t:Importance>
447
+ ${flag}
448
+ </t:Message>`;
449
+ }
450
+
451
+ export const mockGetEmailsResponse = soapResponse(`
452
+ <m:FindItemResponse>
453
+ <m:ResponseCode>NoError</m:ResponseCode>
454
+ <m:RootFolder>
455
+ <t:TotalItemsInView>2</t:TotalItemsInView>
456
+ <m:Items>
457
+ ${makeEmailItem({
458
+ id: 'email-1',
459
+ subject: 'Hello World',
460
+ fromName: 'Alice',
461
+ fromEmail: 'alice@example.com',
462
+ to: 'test@example.com',
463
+ body: 'This is a test email body.',
464
+ receivedDateTime: '2026-03-30T09:00:00Z',
465
+ isRead: false,
466
+ importance: 'Normal'
467
+ })}
468
+ ${makeEmailItem({
469
+ id: 'email-2',
470
+ subject: 'Meeting Tomorrow',
471
+ fromName: 'Bob',
472
+ fromEmail: 'bob@example.com',
473
+ to: 'test@example.com',
474
+ body: "Don't forget our meeting tomorrow.",
475
+ receivedDateTime: '2026-03-29T14:30:00Z',
476
+ isRead: true,
477
+ hasAttachments: true,
478
+ importance: 'High',
479
+ flagStatus: 'Flagged'
480
+ })}
481
+ </m:Items>
482
+ </m:RootFolder>
483
+ </m:FindItemResponse>
484
+ `);
485
+
486
+ export const mockGetEmailsEmptyResponse = soapResponse(`
487
+ <m:FindItemResponse>
488
+ <m:ResponseCode>NoError</m:ResponseCode>
489
+ <m:RootFolder>
490
+ <t:TotalItemsInView>0</t:TotalItemsInView>
491
+ <m:Items />
492
+ </m:RootFolder>
493
+ </m:FindItemResponse>
494
+ `);
495
+
496
+ export const mockGetEmailDetailResponse = soapResponse(`
497
+ <m:GetItemResponse>
498
+ <m:ResponseCode>NoError</m:ResponseCode>
499
+ <m:Items>
500
+ <t:Message>
501
+ <t:ItemId Id="email-detail-1" ChangeKey="emailDetailCK1" />
502
+ <t:From><t:Mailbox><t:Name>Alice</t:Name><t:EmailAddress>alice@example.com</t:EmailAddress></t:Mailbox></t:From>
503
+ <t:ToRecipients><t:Mailbox><t:EmailAddress>test@example.com</t:EmailAddress></t:Mailbox></t:ToRecipients>
504
+ <t:CcRecipients><t:Mailbox><t:EmailAddress>cc@example.com</t:EmailAddress></t:Mailbox></t:CcRecipients>
505
+ <t:Subject>Hello World</t:Subject>
506
+ <t:Body BodyType="Text">This is the full email body.</t:Body>
507
+ <t:DateTimeReceived>2026-03-30T09:00:00Z</t:DateTimeReceived>
508
+ <t:IsRead>false</t:IsRead>
509
+ <t:HasAttachments>true</t:HasAttachments>
510
+ <t:Importance>Normal</t:Importance>
511
+ </t:Message>
512
+ </m:Items>
513
+ </m:GetItemResponse>
514
+ `);
515
+
516
+ export const mockGetAttachmentsResponse = soapResponse(`
517
+ <m:GetAttachmentsResponse>
518
+ <m:ResponseCode>NoError</m:ResponseCode>
519
+ <m:Attachments>
520
+ <t:FileAttachment>
521
+ <t:AttachmentId Id="att-1" />
522
+ <t:Name>document.pdf</t:Name>
523
+ <t:Size>102400</t:Size>
524
+ <t:IsInline>false</t:IsInline>
525
+ <t:ContentId>doc.pdf</t:ContentId>
526
+ </t:FileAttachment>
527
+ </m:Attachments>
528
+ </m:GetAttachmentsResponse>
529
+ `);
530
+
531
+ export const mockUpdateEmailResponse = soapResponse(`
532
+ <m:UpdateItemResponse>
533
+ <m:ResponseCode>NoError</m:ResponseCode>
534
+ <m:Items>
535
+ <t:Message>
536
+ <t:ItemId Id="email-update-1" ChangeKey="updatedCK" />
537
+ </t:Message>
538
+ </m:Items>
539
+ </m:UpdateItemResponse>
540
+ `);
541
+
542
+ export const mockMoveEmailResponse = soapResponse(`
543
+ <m:MoveItemResponse>
544
+ <m:ResponseCode>NoError</m:ResponseCode>
545
+ <m:Items>
546
+ <t:Message>
547
+ <t:ItemId Id="email-moved-1" ChangeKey="movedCK" />
548
+ </t:Message>
549
+ </m:Items>
550
+ </m:MoveItemResponse>
551
+ `);
552
+
553
+ export const mockSendEmailResponse = soapResponse(`
554
+ <m:SendItemResponse>
555
+ <m:ResponseCode>NoError</m:ResponseCode>
556
+ </m:SendItemResponse>
557
+ `);
558
+
559
+ export const mockReplyToEmailResponse = soapResponse(`
560
+ <m:CreateItemResponse>
561
+ <m:ResponseCode>NoError</m:ResponseCode>
562
+ <m:Items>
563
+ <t:Message>
564
+ <t:ItemId Id="reply-draft-1" ChangeKey="replyCK" />
565
+ </t:Message>
566
+ </m:Items>
567
+ </m:CreateItemResponse>
568
+ `);
569
+
570
+ export const mockForwardEmailResponse = soapResponse(`
571
+ <m:ForwardItemResponse>
572
+ <m:ResponseCode>NoError</m:ResponseCode>
573
+ </m:ForwardItemResponse>
574
+ `);
575
+
576
+ export const mockGetMailFoldersResponse = soapResponse(`
577
+ <m:GetFolderResponse>
578
+ <m:ResponseCode>NoError</m:ResponseCode>
579
+ <m:Folders>
580
+ <t:Folder>
581
+ <t:FolderId Id="inbox-id" />
582
+ <t:DisplayName>Inbox</t:DisplayName>
583
+ <t:TotalCount>10</t:TotalCount>
584
+ <t:UnreadCount>3</t:UnreadCount>
585
+ <t:ChildFolderCount>0</t:ChildFolderCount>
586
+ </t:Folder>
587
+ <t:Folder>
588
+ <t:FolderId Id="drafts-id" />
589
+ <t:DisplayName>Drafts</t:DisplayName>
590
+ <t:TotalCount>2</t:TotalCount>
591
+ <t:UnreadCount>0</t:UnreadCount>
592
+ <t:ChildFolderCount>0</t:ChildFolderCount>
593
+ </t:Folder>
594
+ <t:Folder>
595
+ <t:FolderId Id="sentitems-id" />
596
+ <t:DisplayName>Sent Items</t:DisplayName>
597
+ <t:TotalCount>25</t:TotalCount>
598
+ <t:UnreadCount>0</t:UnreadCount>
599
+ <t:ChildFolderCount>0</t:ChildFolderCount>
600
+ </t:Folder>
601
+ <t:Folder>
602
+ <t:FolderId Id="deleteditems-id" />
603
+ <t:DisplayName>Deleted Items</t:DisplayName>
604
+ <t:TotalCount>5</t:TotalCount>
605
+ <t:UnreadCount>0</t:UnreadCount>
606
+ <t:ChildFolderCount>0</t:ChildFolderCount>
607
+ </t:Folder>
608
+ <t:Folder>
609
+ <t:FolderId Id="archive-id" />
610
+ <t:DisplayName>Archive</t:DisplayName>
611
+ <t:TotalCount>8</t:TotalCount>
612
+ <t:UnreadCount>1</t:UnreadCount>
613
+ <t:ChildFolderCount>0</t:ChildFolderCount>
614
+ </t:Folder>
615
+ <t:Folder>
616
+ <t:FolderId Id="custom-id" />
617
+ <t:DisplayName>My Custom Folder</t:DisplayName>
618
+ <t:TotalCount>4</t:TotalCount>
619
+ <t:UnreadCount>0</t:UnreadCount>
620
+ <t:ChildFolderCount>0</t:ChildFolderCount>
621
+ </t:Folder>
622
+ </m:Folders>
623
+ </m:GetFolderResponse>
624
+ `);
625
+
626
+ export const mockCreateMailFolderResponse = soapResponse(`
627
+ <m:CreateFolderResponse>
628
+ <m:ResponseCode>NoError</m:ResponseCode>
629
+ <m:Folders>
630
+ <t:Folder>
631
+ <t:FolderId Id="new-folder-id" />
632
+ <t:DisplayName>New Folder</t:DisplayName>
633
+ <t:TotalCount>0</t:TotalCount>
634
+ <t:UnreadCount>0</t:UnreadCount>
635
+ <t:ChildFolderCount>0</t:ChildFolderCount>
636
+ </t:Folder>
637
+ </m:Folders>
638
+ </m:CreateFolderResponse>
639
+ `);
640
+
641
+ export const mockUpdateMailFolderResponse = soapResponse(`
642
+ <m:UpdateFolderResponse>
643
+ <m:ResponseCode>NoError</m:ResponseCode>
644
+ <m:Folders>
645
+ <t:Folder>
646
+ <t:FolderId Id="folder-updated-id" />
647
+ <t:DisplayName>Renamed Folder</t:DisplayName>
648
+ </t:Folder>
649
+ </m:Folders>
650
+ </m:UpdateFolderResponse>
651
+ `);
652
+
653
+ export const mockDeleteMailFolderResponse = soapResponse(`
654
+ <m:DeleteFolderResponse>
655
+ <m:ResponseCode>NoError</m:ResponseCode>
656
+ </m:DeleteFolderResponse>
657
+ `);
658
+
659
+ // ─── drafts ────────────────────────────────────────────────────────────────
660
+
661
+ export const mockGetDraftsResponse = soapResponse(`
662
+ <m:FindItemResponse>
663
+ <m:ResponseCode>NoError</m:ResponseCode>
664
+ <m:RootFolder>
665
+ <t:TotalItemsInView>2</t:TotalItemsInView>
666
+ <m:Items>
667
+ ${makeEmailItem({
668
+ id: 'draft-1',
669
+ subject: 'Draft Email One',
670
+ to: 'recipient@example.com',
671
+ body: 'Draft body content.',
672
+ receivedDateTime: '2026-03-30T10:00:00Z'
673
+ })}
674
+ ${makeEmailItem({
675
+ id: 'draft-2',
676
+ subject: 'Draft Email Two',
677
+ to: 'another@example.com',
678
+ body: 'Another draft.',
679
+ receivedDateTime: '2026-03-30T11:00:00Z'
680
+ })}
681
+ </m:Items>
682
+ </m:RootFolder>
683
+ </m:FindItemResponse>
684
+ `);
685
+
686
+ export const mockCreateDraftResponse = soapResponse(`
687
+ <m:CreateItemResponse>
688
+ <m:ResponseCode>NoError</m:ResponseCode>
689
+ <m:Items>
690
+ <t:Message>
691
+ <t:ItemId Id="new-draft-id-123" ChangeKey="draftCK123" />
692
+ <t:Subject>New Draft</t:Subject>
693
+ </t:Message>
694
+ </m:Items>
695
+ </m:CreateItemResponse>
696
+ `);
697
+
698
+ export const mockUpdateDraftResponse = soapResponse(`
699
+ <m:UpdateItemResponse>
700
+ <m:ResponseCode>NoError</m:ResponseCode>
701
+ <m:Items>
702
+ <t:Message>
703
+ <t:ItemId Id="draft-edit-id" ChangeKey="draftEditCK" />
704
+ </t:Message>
705
+ </m:Items>
706
+ </m:UpdateItemResponse>
707
+ `);
708
+
709
+ export const mockSendDraftResponse = soapResponse(`
710
+ <m:SendItemResponse>
711
+ <m:ResponseCode>NoError</m:ResponseCode>
712
+ </m:SendItemResponse>
713
+ `);
714
+
715
+ export const mockDeleteDraftResponse = soapResponse(`
716
+ <m:DeleteItemResponse>
717
+ <m:ResponseCode>NoError</m:ResponseCode>
718
+ </m:DeleteItemResponse>
719
+ `);
720
+
721
+ export const mockAddAttachmentResponse = soapResponse(`
722
+ <m:CreateAttachmentResponse>
723
+ <m:ResponseCode>NoError</m:ResponseCode>
724
+ <m:Attachments>
725
+ <t:FileAttachment>
726
+ <t:AttachmentId Id="new-att-id" />
727
+ <t:Name>attachment.pdf</t:Name>
728
+ </t:FileAttachment>
729
+ </m:Attachments>
730
+ <t:RootItemId Id="new-draft-id-123" ChangeKey="afterAttachCK" />
731
+ </m:CreateAttachmentResponse>
732
+ `);
733
+
734
+ // ─── rooms ─────────────────────────────────────────────────────────────────
735
+
736
+ export const mockGetRoomsResponse = soapResponse(`
737
+ <m:GetRoomListsResponse>
738
+ <m:ResponseCode>NoError</m:ResponseCode>
739
+ <m:RoomLists>
740
+ <t:Address>
741
+ <t:EmailAddress>rooms@example.com</t:EmailAddress>
742
+ </t:Address>
743
+ </m:RoomLists>
744
+ </m:GetRoomListsResponse>
745
+ `);
746
+
747
+ export const mockGetRoomsFromListResponse = soapResponse(`
748
+ <m:GetRoomsResponse>
749
+ <m:ResponseCode>NoError</m:ResponseCode>
750
+ <m:Rooms>
751
+ <t:Room>
752
+ <t:Id>room-1</t:Id>
753
+ <t:Name>Conference Room Alpha</t:Name>
754
+ <t:EmailAddress>conf-alpha@example.com</t:EmailAddress>
755
+ </t:Room>
756
+ <t:Room>
757
+ <t:Id>room-2</t:Id>
758
+ <t:Name>Conference Room Beta</t:Name>
759
+ <t:EmailAddress>conf-beta@example.com</t:EmailAddress>
760
+ </t:Room>
761
+ </m:Rooms>
762
+ </m:GetRoomsResponse>
763
+ `);
764
+
765
+ export const mockSearchRoomsResponse = soapResponse(`
766
+ <m:ExpandDLResponse>
767
+ <m:ResponseCode>NoError</m:ResponseCode>
768
+ <m:DLbl>Rooms</m:DLbl>
769
+ <m:AddressList>
770
+ <t:Mailbox>
771
+ <t:Name>Conference Room Alpha</t:Name>
772
+ <t:EmailAddress>conf-alpha@example.com</t:EmailAddress>
773
+ </t:Mailbox>
774
+ </m:AddressList>
775
+ </m:ExpandDLResponse>
776
+ `);
777
+
778
+ export const mockIsRoomFreeResponse = soapResponse(`
779
+ <m:GetRoomListsResponse>
780
+ <m:ResponseCode>NoError</m:ResponseCode>
781
+ </m:GetRoomListsResponse>
782
+ `);
783
+
784
+ // ─── Graph API (OneDrive/files) ─────────────────────────────────────────────
785
+
786
+ export const mockGraphListFilesResponse = {
787
+ value: [
788
+ {
789
+ id: 'drive-item-1',
790
+ name: 'Document.pdf',
791
+ size: 102400,
792
+ lastModifiedDateTime: '2026-03-29T12:00:00Z',
793
+ webUrl: 'https://example.sharepoint.com/doc.pdf',
794
+ file: { mimeType: 'application/pdf' },
795
+ folder: undefined
796
+ },
797
+ {
798
+ id: 'drive-item-2',
799
+ name: 'My Folder',
800
+ size: undefined,
801
+ lastModifiedDateTime: '2026-03-28T09:00:00Z',
802
+ webUrl: 'https://example.sharepoint.com/folder',
803
+ file: undefined,
804
+ folder: { childCount: 5 }
805
+ }
806
+ ]
807
+ };
808
+
809
+ export const mockGraphSearchFilesResponse = {
810
+ value: [
811
+ {
812
+ id: 'drive-item-3',
813
+ name: 'Report.xlsx',
814
+ size: 51200,
815
+ lastModifiedDateTime: '2026-03-27T15:00:00Z',
816
+ webUrl: 'https://example.sharepoint.com/report.xlsx',
817
+ file: { mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
818
+ folder: undefined
819
+ }
820
+ ]
821
+ };
822
+
823
+ export const mockGraphGetFileMetadataResponse = {
824
+ id: 'drive-item-1',
825
+ name: 'Report.docx',
826
+ size: 102400,
827
+ createdDateTime: '2026-03-20T10:00:00Z',
828
+ lastModifiedDateTime: '2026-03-29T12:00:00Z',
829
+ webUrl: 'https://example.sharepoint.com/report.docx',
830
+ file: { mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' },
831
+ folder: undefined,
832
+ parentReference: { driveId: 'drive-1', id: 'root', path: '/drive/root' }
833
+ };
834
+
835
+ export const mockGraphUploadResponse = {
836
+ id: 'new-drive-item-id',
837
+ name: 'Uploaded.txt',
838
+ size: 1024,
839
+ lastModifiedDateTime: '2026-03-30T12:00:00Z',
840
+ webUrl: 'https://example.sharepoint.com/uploaded.txt',
841
+ file: { mimeType: 'text/plain' },
842
+ folder: undefined
843
+ };
844
+
845
+ export const mockGraphDeleteResponse = {};
846
+ export const mockGraphShareResponse = { id: 'share-link-id', webUrl: 'https://example.sharepoint.com/share/abc123' };
847
+ export const mockGraphCollabResponse = {
848
+ item: { id: 'drive-item-1', name: 'Report.docx' },
849
+ link: { webUrl: 'https://example.sharepoint.com/collab' },
850
+ collaborationUrl: 'https://office.com/collab?ItemID=drive-item-1',
851
+ lockAcquired: false
852
+ };
853
+ export const mockGraphCheckinResponse = {
854
+ item: { id: 'checkin-item-id', name: 'CheckedIn.docx' },
855
+ checkedIn: true,
856
+ comment: 'Checked in'
857
+ };
858
+ export const mockGraphCreateUploadSessionResponse = {
859
+ uploadUrl: 'https://upload.microsoft.com/upload-session/abc',
860
+ expirationDateTime: '2026-03-31T12:00:00Z'
861
+ };