@littlebearapps/outlook-assistant 3.3.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 (52) hide show
  1. package/.env.example +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +422 -0
  4. package/advanced/index.js +652 -0
  5. package/auth/index.js +32 -0
  6. package/auth/oauth-server.js +233 -0
  7. package/auth/token-manager.js +105 -0
  8. package/auth/token-storage.js +359 -0
  9. package/auth/tools.js +159 -0
  10. package/calendar/accept.js +72 -0
  11. package/calendar/cancel.js +72 -0
  12. package/calendar/create.js +115 -0
  13. package/calendar/decline.js +72 -0
  14. package/calendar/delete.js +67 -0
  15. package/calendar/index.js +130 -0
  16. package/calendar/list.js +108 -0
  17. package/categories/index.js +955 -0
  18. package/config.js +95 -0
  19. package/contacts/index.js +754 -0
  20. package/email/attachments.js +365 -0
  21. package/email/conversations.js +666 -0
  22. package/email/delta.js +210 -0
  23. package/email/export.js +572 -0
  24. package/email/folder-utils.js +192 -0
  25. package/email/headers.js +344 -0
  26. package/email/index.js +537 -0
  27. package/email/list.js +136 -0
  28. package/email/mark-as-read.js +114 -0
  29. package/email/mime.js +286 -0
  30. package/email/read.js +161 -0
  31. package/email/search.js +628 -0
  32. package/email/send.js +169 -0
  33. package/folder/create.js +137 -0
  34. package/folder/delete.js +108 -0
  35. package/folder/index.js +112 -0
  36. package/folder/list.js +289 -0
  37. package/folder/move.js +186 -0
  38. package/folder/stats.js +322 -0
  39. package/index.js +162 -0
  40. package/llms.txt +76 -0
  41. package/outlook-auth-server.js +384 -0
  42. package/package.json +97 -0
  43. package/rules/create.js +273 -0
  44. package/rules/index.js +276 -0
  45. package/rules/list.js +216 -0
  46. package/settings/index.js +678 -0
  47. package/utils/field-presets.js +311 -0
  48. package/utils/graph-api.js +268 -0
  49. package/utils/mock-data.js +154 -0
  50. package/utils/odata-helpers.js +33 -0
  51. package/utils/response-formatter.js +457 -0
  52. package/utils/safety.js +123 -0
@@ -0,0 +1,652 @@
1
+ /**
2
+ * Advanced module for Outlook Assistant server
3
+ *
4
+ * Provides:
5
+ * - Shared mailbox access
6
+ * - Meeting room search
7
+ *
8
+ * Note: Message flag handlers are still exported from here but their tool
9
+ * definitions have moved to the email module's `update-email` tool.
10
+ */
11
+ const { callGraphAPI } = require('../utils/graph-api');
12
+ const { ensureAuthenticated } = require('../auth');
13
+ const { FIELD_PRESETS } = require('../utils/field-presets');
14
+
15
+ /**
16
+ * Format an email for display (simplified)
17
+ */
18
+ function formatEmail(email, verbosity = 'standard') {
19
+ if (verbosity === 'minimal') {
20
+ return {
21
+ id: email.id,
22
+ subject: email.subject,
23
+ from: email.from?.emailAddress?.address,
24
+ };
25
+ }
26
+
27
+ return {
28
+ id: email.id,
29
+ subject: email.subject,
30
+ from: email.from?.emailAddress
31
+ ? {
32
+ name: email.from.emailAddress.name,
33
+ address: email.from.emailAddress.address,
34
+ }
35
+ : null,
36
+ receivedDateTime: email.receivedDateTime,
37
+ isRead: email.isRead,
38
+ hasAttachments: email.hasAttachments,
39
+ flag: email.flag,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Access shared mailbox handler
45
+ * Requires Mail.Read.Shared permission
46
+ */
47
+ async function handleAccessSharedMailbox(args) {
48
+ const { sharedMailbox, folder, count, outputVerbosity } = args;
49
+
50
+ if (!sharedMailbox) {
51
+ return {
52
+ content: [
53
+ {
54
+ type: 'text',
55
+ text: "Shared mailbox email address is required (e.g., 'shared@company.com').",
56
+ },
57
+ ],
58
+ };
59
+ }
60
+
61
+ const mailFolder = folder || 'inbox';
62
+ const pageSize = Math.min(count || 25, 50);
63
+ const verbosity = outputVerbosity || 'standard';
64
+
65
+ try {
66
+ const accessToken = await ensureAuthenticated();
67
+
68
+ // Build endpoint for shared mailbox
69
+ const endpoint = `users/${sharedMailbox}/mailFolders/${mailFolder}/messages`;
70
+ const fieldSet = verbosity === 'full' ? 'read' : 'list';
71
+ const queryParams = {
72
+ $top: pageSize.toString(),
73
+ $orderby: 'receivedDateTime desc',
74
+ $select: FIELD_PRESETS[fieldSet].join(','),
75
+ };
76
+
77
+ const response = await callGraphAPI(
78
+ accessToken,
79
+ 'GET',
80
+ endpoint,
81
+ null,
82
+ queryParams
83
+ );
84
+
85
+ const messages = response.value || [];
86
+
87
+ if (messages.length === 0) {
88
+ return {
89
+ content: [
90
+ {
91
+ type: 'text',
92
+ text: `No emails found in ${sharedMailbox}/${mailFolder}.\n\nNote: Make sure you have access to this shared mailbox and the Mail.Read.Shared permission is granted.`,
93
+ },
94
+ ],
95
+ };
96
+ }
97
+
98
+ let output = [];
99
+ output.push(`# Shared Mailbox: ${sharedMailbox}`);
100
+ output.push(`**Folder**: ${mailFolder} | **Count**: ${messages.length}\n`);
101
+
102
+ if (verbosity === 'minimal') {
103
+ messages.forEach((msg, i) => {
104
+ output.push(`${i + 1}. ${msg.subject}`);
105
+ output.push(` From: ${msg.from?.emailAddress?.address || 'Unknown'}`);
106
+ });
107
+ } else {
108
+ output.push('| # | Subject | From | Date | Read |');
109
+ output.push('|---|---------|------|------|------|');
110
+ messages.forEach((msg, i) => {
111
+ const date = new Date(msg.receivedDateTime).toLocaleDateString();
112
+ const from =
113
+ msg.from?.emailAddress?.name ||
114
+ msg.from?.emailAddress?.address ||
115
+ 'Unknown';
116
+ const read = msg.isRead ? 'Y' : 'N';
117
+ output.push(
118
+ `| ${i + 1} | ${msg.subject?.substring(0, 40)}${msg.subject?.length > 40 ? '...' : ''} | ${from.substring(0, 20)} | ${date} | ${read} |`
119
+ );
120
+ });
121
+ }
122
+
123
+ if (verbosity === 'full') {
124
+ output.push('\n## Message IDs');
125
+ messages.forEach((msg, i) => {
126
+ output.push(`${i + 1}. \`${msg.id}\``);
127
+ });
128
+ }
129
+
130
+ return {
131
+ content: [
132
+ {
133
+ type: 'text',
134
+ text: output.join('\n'),
135
+ },
136
+ ],
137
+ _meta: {
138
+ sharedMailbox,
139
+ folder: mailFolder,
140
+ count: messages.length,
141
+ messages: messages.map((m) => formatEmail(m, verbosity)),
142
+ },
143
+ };
144
+ } catch (error) {
145
+ if (error.message === 'Authentication required') {
146
+ return {
147
+ content: [
148
+ {
149
+ type: 'text',
150
+ text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
151
+ },
152
+ ],
153
+ };
154
+ }
155
+
156
+ if (
157
+ error.message.includes('Access is denied') ||
158
+ error.message.includes('403')
159
+ ) {
160
+ return {
161
+ content: [
162
+ {
163
+ type: 'text',
164
+ text: `Access denied to shared mailbox "${sharedMailbox}".\n\n**Possible causes:**\n- You don't have access to this shared mailbox\n- The Mail.Read.Shared permission is not granted\n- The shared mailbox address is incorrect`,
165
+ },
166
+ ],
167
+ };
168
+ }
169
+
170
+ if (error.message.includes('not found') || error.message.includes('404')) {
171
+ return {
172
+ content: [
173
+ {
174
+ type: 'text',
175
+ text: `Shared mailbox "${sharedMailbox}" not found. Please verify the email address.`,
176
+ },
177
+ ],
178
+ };
179
+ }
180
+
181
+ return {
182
+ content: [
183
+ {
184
+ type: 'text',
185
+ text: `Error accessing shared mailbox: ${error.message}`,
186
+ },
187
+ ],
188
+ };
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Set message flag handler
194
+ */
195
+ async function handleSetMessageFlag(args) {
196
+ const {
197
+ messageId,
198
+ messageIds,
199
+ dueDateTime,
200
+ startDateTime,
201
+ reminderDateTime: _reminderDateTime,
202
+ } = args;
203
+
204
+ // Support single ID or array
205
+ const ids = messageIds || (messageId ? [messageId] : []);
206
+
207
+ if (ids.length === 0) {
208
+ return {
209
+ content: [
210
+ {
211
+ type: 'text',
212
+ text: 'Message ID (messageId) or IDs (messageIds) required.',
213
+ },
214
+ ],
215
+ };
216
+ }
217
+
218
+ try {
219
+ const accessToken = await ensureAuthenticated();
220
+
221
+ // Build flag object
222
+ const flag = {
223
+ flagStatus: 'flagged',
224
+ };
225
+
226
+ if (dueDateTime) {
227
+ flag.dueDateTime = {
228
+ dateTime: new Date(dueDateTime).toISOString(),
229
+ timeZone: 'UTC',
230
+ };
231
+ }
232
+
233
+ if (startDateTime) {
234
+ flag.startDateTime = {
235
+ dateTime: new Date(startDateTime).toISOString(),
236
+ timeZone: 'UTC',
237
+ };
238
+ }
239
+
240
+ // Process all messages
241
+ const results = [];
242
+ const errors = [];
243
+
244
+ for (const id of ids) {
245
+ try {
246
+ await callGraphAPI(accessToken, 'PATCH', `me/messages/${id}`, {
247
+ flag,
248
+ });
249
+ results.push({ id, success: true });
250
+ } catch (err) {
251
+ errors.push({ id, error: err.message });
252
+ }
253
+ }
254
+
255
+ let output = [];
256
+
257
+ if (results.length > 0) {
258
+ output.push(`Flagged ${results.length} message(s) for follow-up`);
259
+
260
+ if (dueDateTime) {
261
+ output.push(`**Due**: ${new Date(dueDateTime).toLocaleString()}`);
262
+ }
263
+ if (startDateTime) {
264
+ output.push(`**Start**: ${new Date(startDateTime).toLocaleString()}`);
265
+ }
266
+ }
267
+
268
+ if (errors.length > 0) {
269
+ output.push(`\n${errors.length} error(s):`);
270
+ errors.forEach((e) => {
271
+ output.push(`- ${e.id.substring(0, 20)}...: ${e.error}`);
272
+ });
273
+ }
274
+
275
+ return {
276
+ content: [
277
+ {
278
+ type: 'text',
279
+ text: output.join('\n'),
280
+ },
281
+ ],
282
+ _meta: {
283
+ successful: results.length,
284
+ failed: errors.length,
285
+ results,
286
+ errors,
287
+ },
288
+ };
289
+ } catch (error) {
290
+ if (error.message === 'Authentication required') {
291
+ return {
292
+ content: [
293
+ {
294
+ type: 'text',
295
+ text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
296
+ },
297
+ ],
298
+ };
299
+ }
300
+ return {
301
+ content: [
302
+ {
303
+ type: 'text',
304
+ text: `Error setting message flag: ${error.message}`,
305
+ },
306
+ ],
307
+ };
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Clear message flag handler
313
+ */
314
+ async function handleClearMessageFlag(args) {
315
+ const { messageId, messageIds, markComplete } = args;
316
+
317
+ // Support single ID or array
318
+ const ids = messageIds || (messageId ? [messageId] : []);
319
+
320
+ if (ids.length === 0) {
321
+ return {
322
+ content: [
323
+ {
324
+ type: 'text',
325
+ text: 'Message ID (messageId) or IDs (messageIds) required.',
326
+ },
327
+ ],
328
+ };
329
+ }
330
+
331
+ try {
332
+ const accessToken = await ensureAuthenticated();
333
+
334
+ // Determine flag status
335
+ const flag = {
336
+ flagStatus: markComplete ? 'complete' : 'notFlagged',
337
+ };
338
+
339
+ // Clear completion date if marking complete
340
+ if (markComplete) {
341
+ flag.completedDateTime = {
342
+ dateTime: new Date().toISOString(),
343
+ timeZone: 'UTC',
344
+ };
345
+ }
346
+
347
+ // Process all messages
348
+ const results = [];
349
+ const errors = [];
350
+
351
+ for (const id of ids) {
352
+ try {
353
+ await callGraphAPI(accessToken, 'PATCH', `me/messages/${id}`, {
354
+ flag,
355
+ });
356
+ results.push({ id, success: true });
357
+ } catch (err) {
358
+ errors.push({ id, error: err.message });
359
+ }
360
+ }
361
+
362
+ const action = markComplete ? 'marked complete' : 'cleared';
363
+ let output = [];
364
+
365
+ if (results.length > 0) {
366
+ output.push(`${results.length} message(s) ${action}`);
367
+ }
368
+
369
+ if (errors.length > 0) {
370
+ output.push(`\n${errors.length} error(s):`);
371
+ errors.forEach((e) => {
372
+ output.push(`- ${e.id.substring(0, 20)}...: ${e.error}`);
373
+ });
374
+ }
375
+
376
+ return {
377
+ content: [
378
+ {
379
+ type: 'text',
380
+ text: output.join('\n'),
381
+ },
382
+ ],
383
+ _meta: {
384
+ action: markComplete ? 'complete' : 'cleared',
385
+ successful: results.length,
386
+ failed: errors.length,
387
+ results,
388
+ errors,
389
+ },
390
+ };
391
+ } catch (error) {
392
+ if (error.message === 'Authentication required') {
393
+ return {
394
+ content: [
395
+ {
396
+ type: 'text',
397
+ text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
398
+ },
399
+ ],
400
+ };
401
+ }
402
+ return {
403
+ content: [
404
+ {
405
+ type: 'text',
406
+ text: `Error clearing message flag: ${error.message}`,
407
+ },
408
+ ],
409
+ };
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Find meeting rooms handler
415
+ */
416
+ async function handleFindMeetingRooms(args) {
417
+ const { query, building, floor, capacity, outputVerbosity } = args;
418
+ const verbosity = outputVerbosity || 'standard';
419
+
420
+ try {
421
+ const accessToken = await ensureAuthenticated();
422
+
423
+ // Try the findRooms endpoint first (may not be available in all tenants)
424
+ let rooms = [];
425
+
426
+ try {
427
+ // Try /places endpoint for room lists
428
+ const placesResponse = await callGraphAPI(
429
+ accessToken,
430
+ 'GET',
431
+ 'places/microsoft.graph.room'
432
+ );
433
+ rooms = placesResponse.value || [];
434
+ } catch (_placesError) {
435
+ // Fall back to findRooms
436
+ try {
437
+ const roomsResponse = await callGraphAPI(
438
+ accessToken,
439
+ 'GET',
440
+ 'me/findRooms'
441
+ );
442
+ rooms = roomsResponse.value || [];
443
+ } catch (findRoomsError) {
444
+ return {
445
+ content: [
446
+ {
447
+ type: 'text',
448
+ text: `Unable to find meeting rooms.\n\n**Note**: This feature requires:\n- Places.Read.All permission\n- Meeting rooms configured in your organization\n\nError: ${findRoomsError.message}`,
449
+ },
450
+ ],
451
+ };
452
+ }
453
+ }
454
+
455
+ // Apply filters
456
+ if (query) {
457
+ const q = query.toLowerCase();
458
+ rooms = rooms.filter(
459
+ (r) =>
460
+ r.displayName?.toLowerCase().includes(q) ||
461
+ r.emailAddress?.toLowerCase().includes(q) ||
462
+ r.nickname?.toLowerCase().includes(q)
463
+ );
464
+ }
465
+
466
+ if (building) {
467
+ const b = building.toLowerCase();
468
+ rooms = rooms.filter((r) => r.building?.toLowerCase().includes(b));
469
+ }
470
+
471
+ if (floor !== undefined) {
472
+ rooms = rooms.filter((r) => r.floorNumber === floor);
473
+ }
474
+
475
+ if (capacity) {
476
+ rooms = rooms.filter((r) => r.capacity >= capacity);
477
+ }
478
+
479
+ if (rooms.length === 0) {
480
+ return {
481
+ content: [
482
+ {
483
+ type: 'text',
484
+ text: 'No meeting rooms found matching your criteria.\n\nTry broadening your search or check if rooms are configured in your organization.',
485
+ },
486
+ ],
487
+ };
488
+ }
489
+
490
+ let output = [];
491
+ output.push(`# Meeting Rooms (${rooms.length})\n`);
492
+
493
+ if (verbosity === 'minimal') {
494
+ rooms.forEach((room) => {
495
+ output.push(
496
+ `- ${room.displayName || room.name} (${room.emailAddress})`
497
+ );
498
+ });
499
+ } else {
500
+ rooms.forEach((room) => {
501
+ output.push(`## ${room.displayName || room.name}`);
502
+ output.push(`**Email**: ${room.emailAddress || 'N/A'}`);
503
+
504
+ if (room.capacity) {
505
+ output.push(`**Capacity**: ${room.capacity}`);
506
+ }
507
+ if (room.building) {
508
+ output.push(`**Building**: ${room.building}`);
509
+ }
510
+ if (room.floorNumber !== undefined) {
511
+ output.push(`**Floor**: ${room.floorNumber}`);
512
+ }
513
+ if (room.floorLabel) {
514
+ output.push(`**Floor Label**: ${room.floorLabel}`);
515
+ }
516
+
517
+ if (verbosity === 'full') {
518
+ if (room.audioDeviceName) {
519
+ output.push(`**Audio**: ${room.audioDeviceName}`);
520
+ }
521
+ if (room.videoDeviceName) {
522
+ output.push(`**Video**: ${room.videoDeviceName}`);
523
+ }
524
+ if (room.displayDeviceName) {
525
+ output.push(`**Display**: ${room.displayDeviceName}`);
526
+ }
527
+ if (room.isWheelChairAccessible !== undefined) {
528
+ output.push(
529
+ `**Wheelchair Accessible**: ${room.isWheelChairAccessible ? 'Yes' : 'No'}`
530
+ );
531
+ }
532
+ }
533
+
534
+ output.push('');
535
+ });
536
+ }
537
+
538
+ return {
539
+ content: [
540
+ {
541
+ type: 'text',
542
+ text: output.join('\n'),
543
+ },
544
+ ],
545
+ _meta: {
546
+ count: rooms.length,
547
+ rooms: rooms,
548
+ },
549
+ };
550
+ } catch (error) {
551
+ if (error.message === 'Authentication required') {
552
+ return {
553
+ content: [
554
+ {
555
+ type: 'text',
556
+ text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
557
+ },
558
+ ],
559
+ };
560
+ }
561
+ return {
562
+ content: [
563
+ {
564
+ type: 'text',
565
+ text: `Error finding meeting rooms: ${error.message}`,
566
+ },
567
+ ],
568
+ };
569
+ }
570
+ }
571
+
572
+ // Consolidated tool definitions (4 → 2, flag tools moved to email/update-email)
573
+ const advancedTools = [
574
+ {
575
+ name: 'access-shared-mailbox',
576
+ description: 'Read emails from a shared mailbox you have access to',
577
+ annotations: {
578
+ title: 'Shared Mailbox',
579
+ readOnlyHint: true,
580
+ openWorldHint: false,
581
+ },
582
+ inputSchema: {
583
+ type: 'object',
584
+ properties: {
585
+ sharedMailbox: {
586
+ type: 'string',
587
+ description: 'Email address of the shared mailbox (required)',
588
+ },
589
+ folder: {
590
+ type: 'string',
591
+ description: 'Folder to read from (default: inbox)',
592
+ },
593
+ count: {
594
+ type: 'number',
595
+ description: 'Number of emails to retrieve (default: 25, max: 50)',
596
+ },
597
+ outputVerbosity: {
598
+ type: 'string',
599
+ enum: ['minimal', 'standard', 'full'],
600
+ description: 'Output detail level (default: standard)',
601
+ },
602
+ },
603
+ required: ['sharedMailbox'],
604
+ },
605
+ handler: handleAccessSharedMailbox,
606
+ },
607
+ {
608
+ name: 'find-meeting-rooms',
609
+ description: 'Search for meeting rooms in your organisation',
610
+ annotations: {
611
+ title: 'Meeting Rooms',
612
+ readOnlyHint: true,
613
+ openWorldHint: false,
614
+ },
615
+ inputSchema: {
616
+ type: 'object',
617
+ properties: {
618
+ query: {
619
+ type: 'string',
620
+ description: 'Search query (room name, email)',
621
+ },
622
+ building: {
623
+ type: 'string',
624
+ description: 'Filter by building name',
625
+ },
626
+ floor: {
627
+ type: 'number',
628
+ description: 'Filter by floor number',
629
+ },
630
+ capacity: {
631
+ type: 'number',
632
+ description: 'Minimum capacity required',
633
+ },
634
+ outputVerbosity: {
635
+ type: 'string',
636
+ enum: ['minimal', 'standard', 'full'],
637
+ description: 'Output detail level (default: standard)',
638
+ },
639
+ },
640
+ required: [],
641
+ },
642
+ handler: handleFindMeetingRooms,
643
+ },
644
+ ];
645
+
646
+ module.exports = {
647
+ advancedTools,
648
+ handleAccessSharedMailbox,
649
+ handleSetMessageFlag,
650
+ handleClearMessageFlag,
651
+ handleFindMeetingRooms,
652
+ };
package/auth/index.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Authentication module for Outlook Assistant server
3
+ */
4
+ const tokenManager = require('./token-manager');
5
+ const { authTools } = require('./tools');
6
+
7
+ /**
8
+ * Ensures the user is authenticated and returns an access token
9
+ * @param {boolean} forceNew - Whether to force a new authentication
10
+ * @returns {Promise<string>} - Access token
11
+ * @throws {Error} - If authentication fails
12
+ */
13
+ async function ensureAuthenticated(forceNew = false) {
14
+ if (forceNew) {
15
+ // Force re-authentication
16
+ throw new Error('Authentication required');
17
+ }
18
+
19
+ // Check for existing token
20
+ const accessToken = tokenManager.getAccessToken();
21
+ if (!accessToken) {
22
+ throw new Error('Authentication required');
23
+ }
24
+
25
+ return accessToken;
26
+ }
27
+
28
+ module.exports = {
29
+ tokenManager,
30
+ authTools,
31
+ ensureAuthenticated,
32
+ };