ms365-mcp-server 1.0.5 → 1.1.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 (4) hide show
  1. package/README.md +214 -116
  2. package/bin/cli.js +2 -2
  3. package/dist/index.js +320 -645
  4. package/package.json +2 -3
package/dist/index.js CHANGED
@@ -55,7 +55,7 @@ function parseArgs() {
55
55
  }
56
56
  const server = new Server({
57
57
  name: "ms365-mcp-server",
58
- version: "1.0.5"
58
+ version: "1.1.0"
59
59
  }, {
60
60
  capabilities: {
61
61
  resources: {
@@ -172,8 +172,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
172
172
  }
173
173
  },
174
174
  {
175
- name: "read_email",
176
- description: "Read email message by ID with advanced content extraction. Returns full email content including headers, body, and attachment information.",
175
+ name: "manage_email",
176
+ description: "UNIFIED EMAIL MANAGEMENT: Read, search, list, mark, move, or delete emails. Combines all email operations in one powerful tool. Supports intelligent partial name matching, folder management, and advanced search criteria.",
177
177
  inputSchema: {
178
178
  type: "object",
179
179
  properties: {
@@ -181,29 +181,32 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
181
181
  type: "string",
182
182
  description: "User ID for multi-user authentication (required if using multi-user mode)"
183
183
  },
184
+ action: {
185
+ type: "string",
186
+ enum: ["read", "search", "list", "mark", "move", "delete", "search_to_me"],
187
+ description: "Action to perform: read (get email by ID), search (find emails), list (folder contents), mark (read/unread), move (to folder), delete (permanently), search_to_me (emails addressed to you)"
188
+ },
184
189
  messageId: {
185
190
  type: "string",
186
- description: "Microsoft 365 message ID to retrieve"
191
+ description: "Email ID (required for: read, mark, move, delete)"
187
192
  },
188
193
  includeAttachments: {
189
194
  type: "boolean",
190
- description: "Whether to include attachment information and content",
195
+ description: "Include attachment info when reading email",
191
196
  default: false
192
- }
193
- },
194
- required: ["messageId"],
195
- additionalProperties: false
196
- }
197
- },
198
- {
199
- name: "search_emails",
200
- description: "Search emails with various criteria including subject, sender, TO/CC recipients, date range, and advanced filtering. Features intelligent partial name matching for senders - just use their first name, last name, or partial name in the 'from' field. Supports complex queries and filtering, including emails addressed to you directly or where you were CC'd.",
201
- inputSchema: {
202
- type: "object",
203
- properties: {
204
- userId: {
197
+ },
198
+ isRead: {
199
+ type: "boolean",
200
+ description: "Mark as read (true) or unread (false) - used with mark action"
201
+ },
202
+ destinationFolderId: {
205
203
  type: "string",
206
- description: "User ID for multi-user authentication (required if using multi-user mode)"
204
+ description: "Destination folder for move action (e.g., 'archive', 'drafts', 'deleteditems')"
205
+ },
206
+ folderId: {
207
+ type: "string",
208
+ description: "Folder to list emails from (default: inbox) - used with list action",
209
+ default: "inbox"
207
210
  },
208
211
  query: {
209
212
  type: "string",
@@ -211,85 +214,27 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
211
214
  },
212
215
  from: {
213
216
  type: "string",
214
- description: "Search for emails from specific sender"
217
+ description: "Search emails from specific sender (supports partial names like 'John' or 'Smith')"
215
218
  },
216
219
  to: {
217
220
  type: "string",
218
- description: "Search for emails to specific recipient (useful for finding emails addressed to you)"
221
+ description: "Search emails to specific recipient"
219
222
  },
220
223
  cc: {
221
224
  type: "string",
222
- description: "Search for emails with specific CC recipient (useful for finding emails where someone was CC'd)"
223
- },
224
- subject: {
225
- type: "string",
226
- description: "Search for emails with specific subject"
227
- },
228
- after: {
229
- type: "string",
230
- description: "Search for emails after date (format: YYYY-MM-DD)"
231
- },
232
- before: {
233
- type: "string",
234
- description: "Search for emails before date (format: YYYY-MM-DD)"
235
- },
236
- hasAttachment: {
237
- type: "boolean",
238
- description: "Filter emails that have attachments"
239
- },
240
- folder: {
241
- type: "string",
242
- description: "Search within specific folder (default: inbox)"
243
- },
244
- isUnread: {
245
- type: "boolean",
246
- description: "Filter for unread emails only"
247
- },
248
- importance: {
249
- type: "string",
250
- enum: ["low", "normal", "high"],
251
- description: "Filter by email importance level"
252
- },
253
- maxResults: {
254
- type: "number",
255
- description: "Maximum number of results to return (default: 50, max: 200)",
256
- minimum: 1,
257
- maximum: 200,
258
- default: 50
259
- }
260
- },
261
- additionalProperties: false
262
- }
263
- },
264
- {
265
- name: "search_emails_to_me",
266
- description: "Search for emails addressed to YOU in both TO and CC fields. This automatically finds all emails where you are a recipient (direct TO or CC'd), without needing to specify your email address.",
267
- inputSchema: {
268
- type: "object",
269
- properties: {
270
- userId: {
271
- type: "string",
272
- description: "User ID for multi-user authentication (required if using multi-user mode)"
273
- },
274
- query: {
275
- type: "string",
276
- description: "General search query using natural language or specific terms"
277
- },
278
- from: {
279
- type: "string",
280
- description: "Search for emails from specific sender"
225
+ description: "Search emails with specific CC recipient"
281
226
  },
282
227
  subject: {
283
228
  type: "string",
284
- description: "Search for emails with specific subject"
229
+ description: "Search emails with specific subject"
285
230
  },
286
231
  after: {
287
232
  type: "string",
288
- description: "Search for emails after date (format: YYYY-MM-DD)"
233
+ description: "Search emails after date (format: YYYY-MM-DD)"
289
234
  },
290
235
  before: {
291
236
  type: "string",
292
- description: "Search for emails before date (format: YYYY-MM-DD)"
237
+ description: "Search emails before date (format: YYYY-MM-DD)"
293
238
  },
294
239
  hasAttachment: {
295
240
  type: "boolean",
@@ -316,97 +261,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
316
261
  default: 50
317
262
  }
318
263
  },
319
- additionalProperties: false
320
- }
321
- },
322
- {
323
- name: "list_emails",
324
- description: "List emails in inbox, sent, or custom folder. Returns basic email information including subjects, senders, and snippets with support for folder navigation.",
325
- inputSchema: {
326
- type: "object",
327
- properties: {
328
- userId: {
329
- type: "string",
330
- description: "User ID for multi-user authentication (required if using multi-user mode)"
331
- },
332
- folderId: {
333
- type: "string",
334
- description: "Folder ID or name (default: inbox). Common values: inbox, sentitems, drafts, deleteditems",
335
- default: "inbox"
336
- },
337
- maxResults: {
338
- type: "number",
339
- description: "Maximum number of emails to retrieve (default: 50, max: 200)",
340
- minimum: 1,
341
- maximum: 200,
342
- default: 50
343
- }
344
- },
345
- additionalProperties: false
346
- }
347
- },
348
- {
349
- name: "mark_email",
350
- description: "Mark email as read or unread. Useful for managing email status and organizing your inbox.",
351
- inputSchema: {
352
- type: "object",
353
- properties: {
354
- userId: {
355
- type: "string",
356
- description: "User ID for multi-user authentication (required if using multi-user mode)"
357
- },
358
- messageId: {
359
- type: "string",
360
- description: "Microsoft 365 message ID to mark"
361
- },
362
- isRead: {
363
- type: "boolean",
364
- description: "True to mark as read, false to mark as unread"
365
- }
366
- },
367
- required: ["messageId", "isRead"],
368
- additionalProperties: false
369
- }
370
- },
371
- {
372
- name: "move_email",
373
- description: "Move email to different folder. Supports moving emails between various Outlook folders for organization.",
374
- inputSchema: {
375
- type: "object",
376
- properties: {
377
- userId: {
378
- type: "string",
379
- description: "User ID for multi-user authentication (required if using multi-user mode)"
380
- },
381
- messageId: {
382
- type: "string",
383
- description: "Microsoft 365 message ID to move"
384
- },
385
- destinationFolderId: {
386
- type: "string",
387
- description: "Destination folder ID or name (e.g., 'archive', 'drafts', 'deleteditems')"
388
- }
389
- },
390
- required: ["messageId", "destinationFolderId"],
391
- additionalProperties: false
392
- }
393
- },
394
- {
395
- name: "delete_email",
396
- description: "Permanently delete an email message. Use with caution as this action cannot be undone.",
397
- inputSchema: {
398
- type: "object",
399
- properties: {
400
- userId: {
401
- type: "string",
402
- description: "User ID for multi-user authentication (required if using multi-user mode)"
403
- },
404
- messageId: {
405
- type: "string",
406
- description: "Microsoft 365 message ID to delete"
407
- }
408
- },
409
- required: ["messageId"],
264
+ required: ["action"],
410
265
  additionalProperties: false
411
266
  }
412
267
  },
@@ -448,8 +303,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
448
303
  }
449
304
  },
450
305
  {
451
- name: "get_contacts",
452
- description: "Retrieve contact list from Microsoft 365. Returns contact information including names, email addresses, and phone numbers.",
306
+ name: "manage_contacts",
307
+ description: "UNIFIED CONTACT MANAGEMENT: Get all contacts or search contacts by name/email. Combines contact operations in one tool.",
453
308
  inputSchema: {
454
309
  type: "object",
455
310
  properties: {
@@ -457,40 +312,23 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
457
312
  type: "string",
458
313
  description: "User ID for multi-user authentication (required if using multi-user mode)"
459
314
  },
460
- maxResults: {
461
- type: "number",
462
- description: "Maximum number of contacts to retrieve (default: 100, max: 500)",
463
- minimum: 1,
464
- maximum: 500,
465
- default: 100
466
- }
467
- },
468
- additionalProperties: false
469
- }
470
- },
471
- {
472
- name: "search_contacts",
473
- description: "Search contacts by name or email address. Useful for finding specific contacts for email addressing.",
474
- inputSchema: {
475
- type: "object",
476
- properties: {
477
- userId: {
315
+ action: {
478
316
  type: "string",
479
- description: "User ID for multi-user authentication (required if using multi-user mode)"
317
+ enum: ["list", "search"],
318
+ description: "Action: list (get all contacts) or search (find specific contacts)",
319
+ default: "list"
480
320
  },
481
321
  query: {
482
322
  type: "string",
483
- description: "Search query for contact name or email address"
323
+ description: "Search query for contact name or email address (required for search action)"
484
324
  },
485
325
  maxResults: {
486
326
  type: "number",
487
- description: "Maximum number of contacts to return (default: 50, max: 200)",
327
+ description: "Maximum number of contacts to return (default: 100 for list, 50 for search, max: 500)",
488
328
  minimum: 1,
489
- maximum: 200,
490
- default: 50
329
+ maximum: 500
491
330
  }
492
331
  },
493
- required: ["query"],
494
332
  additionalProperties: false
495
333
  }
496
334
  }
@@ -527,92 +365,28 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
527
365
  }
528
366
  }
529
367
  ];
530
- // Add one-time authentication tool for single-user mode
531
- const oneTimeAuthTools = [
532
- {
533
- name: "get_auth_link",
534
- description: "Get a clickable Microsoft 365 authentication link without auto-opening browser. Perfect for manual authentication control.",
535
- inputSchema: {
536
- type: "object",
537
- properties: {
538
- force: {
539
- type: "boolean",
540
- description: "Force new authentication link even if already authenticated (default: false)",
541
- default: false
542
- }
543
- },
544
- additionalProperties: false
545
- }
546
- }
547
- ];
548
- // Enhanced authentication tools with device code flow
549
- const enhancedAuthTools = [
368
+ // Consolidated authentication tools
369
+ const authTools = [
550
370
  {
551
- name: "authenticate_with_device_code",
552
- description: "Complete device code authentication in MCP. First call shows device code, second call (after browser auth) completes the process. RECOMMENDED method.",
553
- inputSchema: {
554
- type: "object",
555
- properties: {
556
- force: {
557
- type: "boolean",
558
- description: "Force new authentication even if already authenticated (default: false)",
559
- default: false
560
- }
561
- },
562
- additionalProperties: false
563
- }
564
- },
565
- {
566
- name: "device_code_login",
567
- description: "Start device code authentication flow and get the URL/code to enter. Shows authentication details immediately in the UI.",
371
+ name: "authenticate",
372
+ description: "UNIFIED AUTHENTICATION: Handle all Microsoft 365 authentication needs. Supports device code flow, status checking, and logout. RECOMMENDED for all auth operations.",
568
373
  inputSchema: {
569
374
  type: "object",
570
375
  properties: {
376
+ action: {
377
+ type: "string",
378
+ enum: ["login", "status", "logout", "device_code", "check_pending"],
379
+ description: "Auth action: login (device code auth), status (check auth status), logout (clear tokens), device_code (get device code only), check_pending (check pending auth)",
380
+ default: "login"
381
+ },
571
382
  force: {
572
383
  type: "boolean",
573
- description: "Force new authentication even if already authenticated (default: false)",
384
+ description: "Force new authentication even if already authenticated (for login action)",
574
385
  default: false
575
- }
576
- },
577
- additionalProperties: false
578
- }
579
- },
580
- {
581
- name: "check_pending_auth",
582
- description: "Check if there's a pending device code authentication and get the URL/code again if needed.",
583
- inputSchema: {
584
- type: "object",
585
- properties: {},
586
- additionalProperties: false
587
- }
588
- },
589
- {
590
- name: "get_device_code",
591
- description: "Get device code information for manual authentication without starting the full flow. Returns URL and code to enter.",
592
- inputSchema: {
593
- type: "object",
594
- properties: {},
595
- additionalProperties: false
596
- }
597
- },
598
- {
599
- name: "verify_authentication",
600
- description: "Verify current authentication status and show account information. Useful for checking if authentication is still valid.",
601
- inputSchema: {
602
- type: "object",
603
- properties: {},
604
- additionalProperties: false
605
- }
606
- },
607
- {
608
- name: "logout",
609
- description: "Log out and clear stored authentication tokens for the current user. This will require re-authentication.",
610
- inputSchema: {
611
- type: "object",
612
- properties: {
386
+ },
613
387
  accountKey: {
614
388
  type: "string",
615
- description: "Account key to logout (default: current user)",
389
+ description: "Account key for logout action (default: current user)",
616
390
  default: "default-user"
617
391
  }
618
392
  },
@@ -623,14 +397,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
623
397
  if (ms365Config.multiUser) {
624
398
  return { tools: [...baseTools, ...multiUserTools] };
625
399
  }
626
- // For single-user mode, include enhanced authentication tools
627
- // Check if user is authenticated using enhanced auth system
628
- const isAuthenticated = await enhancedMS365Auth.isAuthenticated();
629
- if (!isAuthenticated) {
630
- return { tools: [...baseTools, ...oneTimeAuthTools, ...enhancedAuthTools] };
631
- }
632
- // If authenticated, still provide auth management tools
633
- return { tools: [...baseTools, ...enhancedAuthTools] };
400
+ // For single-user mode, always include auth tools
401
+ return { tools: [...baseTools, ...authTools] };
634
402
  });
635
403
  /**
636
404
  * Handle tool execution requests
@@ -639,312 +407,257 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
639
407
  const { name, arguments: args } = request.params;
640
408
  try {
641
409
  switch (name) {
642
- // ============ ENHANCED AUTHENTICATION TOOLS ============
643
- case "authenticate_with_device_code":
644
- try {
645
- // Check if already authenticated
646
- if (await enhancedMS365Auth.isAuthenticated() && !args?.force) {
410
+ // ============ UNIFIED AUTHENTICATION TOOL ============
411
+ case "authenticate":
412
+ const action = args?.action || 'login';
413
+ switch (action) {
414
+ case "login":
415
+ try {
416
+ // Check if already authenticated
417
+ if (await enhancedMS365Auth.isAuthenticated() && !args?.force) {
418
+ return {
419
+ content: [
420
+ {
421
+ type: "text",
422
+ text: `āœ… Already authenticated with Microsoft 365! Use force: true to re-authenticate.`
423
+ }
424
+ ]
425
+ };
426
+ }
427
+ // First, try to complete any existing pending authentication
428
+ const existingResult = await enhancedMS365Auth.completeDeviceCodeAuth();
429
+ if (existingResult) {
430
+ const currentUser = await enhancedMS365Auth.getCurrentUser();
431
+ return {
432
+ content: [
433
+ {
434
+ type: "text",
435
+ text: `āœ… Device code authentication completed successfully!\n\nšŸ‘¤ User: ${currentUser || 'authenticated-user'}\nšŸ” Status: Valid\n\nšŸš€ You can now use all Microsoft 365 email features!`
436
+ }
437
+ ]
438
+ };
439
+ }
440
+ // Check if there's already a pending device code
441
+ let deviceCodeInfo = await enhancedMS365Auth.getPendingDeviceCodeInfo();
442
+ if (!deviceCodeInfo || args?.force) {
443
+ // No pending code or force new one, start fresh
444
+ deviceCodeInfo = await enhancedMS365Auth.startDeviceCodeAuth();
445
+ }
446
+ // Return device code information immediately (MCP pattern)
447
+ return {
448
+ content: [
449
+ {
450
+ type: "text",
451
+ text: `šŸ” Microsoft 365 Device Code Authentication\n\nšŸ“± Visit: ${deviceCodeInfo.verificationUri}\nšŸ”‘ Enter this code: ${deviceCodeInfo.userCode}\n\nā³ After completing authentication in your browser, call this tool again to finish the process.\n\nšŸ’” This code will expire in 15 minutes.`
452
+ }
453
+ ]
454
+ };
455
+ }
456
+ catch (error) {
457
+ return {
458
+ content: [
459
+ {
460
+ type: "text",
461
+ text: `āŒ Authentication failed: ${error.message}\n\nšŸ’” Try again or use CLI: node dist/index.js --login`
462
+ }
463
+ ]
464
+ };
465
+ }
466
+ case "status":
467
+ const isAuthenticated = await enhancedMS365Auth.isAuthenticated();
468
+ const currentUser = await enhancedMS365Auth.getCurrentUser();
469
+ const storageInfo = enhancedMS365Auth.getStorageInfo();
470
+ const tokenInfo = await enhancedMS365Auth.getTokenExpirationInfo();
471
+ let statusText = `šŸ“Š Microsoft 365 Authentication Status\n\nšŸ” Authentication: ${isAuthenticated ? 'āœ… Valid' : 'āŒ Not authenticated'}\nšŸ‘¤ Current User: ${currentUser || 'None'}\nšŸ’¾ Storage method: ${storageInfo.method}\nšŸ“ Storage location: ${storageInfo.location}`;
472
+ if (isAuthenticated) {
473
+ statusText += `\nā° Token expires in: ${tokenInfo.expiresInMinutes} minutes`;
474
+ if (tokenInfo.needsRefresh) {
475
+ statusText += `\nāš ļø Token will be refreshed automatically on next operation`;
476
+ }
477
+ }
478
+ else {
479
+ statusText += `\nšŸ’” Use "authenticate" with action: login to sign in`;
480
+ }
647
481
  return {
648
482
  content: [
649
483
  {
650
484
  type: "text",
651
- text: `āœ… Already authenticated with Microsoft 365! Use force: true to re-authenticate.`
485
+ text: statusText
652
486
  }
653
487
  ]
654
488
  };
655
- }
656
- // First, try to complete any existing pending authentication
657
- const existingResult = await enhancedMS365Auth.completeDeviceCodeAuth();
658
- if (existingResult) {
659
- const currentUser = await enhancedMS365Auth.getCurrentUser();
489
+ case "logout":
490
+ const accountKey = args?.accountKey;
491
+ const wasAuthenticated = await enhancedMS365Auth.isAuthenticated();
492
+ await enhancedMS365Auth.resetAuth();
660
493
  return {
661
494
  content: [
662
495
  {
663
496
  type: "text",
664
- text: `āœ… Device code authentication completed successfully!\n\nšŸ‘¤ User: ${currentUser || 'authenticated-user'}\nšŸ” Status: Valid\n\nšŸš€ You can now use all Microsoft 365 email features!`
497
+ text: wasAuthenticated ?
498
+ `āœ… Successfully logged out from Microsoft 365.` :
499
+ `ā„¹ļø No active authentication found.`
665
500
  }
666
501
  ]
667
502
  };
668
- }
669
- // Check if there's already a pending device code
670
- let deviceCodeInfo = await enhancedMS365Auth.getPendingDeviceCodeInfo();
671
- if (!deviceCodeInfo || args?.force) {
672
- // No pending code or force new one, start fresh
673
- deviceCodeInfo = await enhancedMS365Auth.startDeviceCodeAuth();
674
- }
675
- // Return device code information immediately (MCP pattern)
676
- return {
677
- content: [
678
- {
679
- type: "text",
680
- text: `šŸ” Microsoft 365 Device Code Authentication\n\nšŸ“± Visit: ${deviceCodeInfo.verificationUri}\nšŸ”‘ Enter this code: ${deviceCodeInfo.userCode}\n\nā³ After completing authentication in your browser, call this tool again to finish the process.\n\nšŸ’” This code will expire in 15 minutes.`
681
- }
682
- ]
683
- };
503
+ case "device_code":
504
+ const deviceCodeInfo = await enhancedMS365Auth.getDeviceCodeInfo();
505
+ return {
506
+ content: [
507
+ {
508
+ type: "text",
509
+ text: `šŸ” Microsoft 365 Device Code Authentication\n\nšŸ“± Visit: ${deviceCodeInfo.verificationUri}\nšŸ”‘ Enter code: ${deviceCodeInfo.userCode}\n\nā³ This code will expire in 15 minutes.`
510
+ }
511
+ ]
512
+ };
513
+ case "check_pending":
514
+ const pendingDeviceCodeState = await enhancedMS365Auth.getPendingDeviceCodeInfo();
515
+ if (pendingDeviceCodeState) {
516
+ return {
517
+ content: [
518
+ {
519
+ type: "text",
520
+ text: `ā³ Pending Device Code Authentication\n\nšŸ“± Visit: ${pendingDeviceCodeState.verificationUri}\nšŸ”‘ Enter this code: ${pendingDeviceCodeState.userCode}\n\nšŸ’” Use "authenticate" with action: login to finish authentication after entering the code.`
521
+ }
522
+ ]
523
+ };
524
+ }
525
+ return {
526
+ content: [
527
+ {
528
+ type: "text",
529
+ text: "ā„¹ļø No pending device code authentication found. Use 'authenticate' with action: login to start a new authentication process."
530
+ }
531
+ ]
532
+ };
533
+ default:
534
+ throw new Error(`Unknown authentication action: ${action}`);
684
535
  }
685
- catch (error) {
686
- return {
687
- content: [
688
- {
689
- type: "text",
690
- text: `āŒ Authentication failed: ${error.message}\n\nšŸ’” Try again or use CLI: node dist/index.js --login`
691
- }
692
- ]
693
- };
694
- }
695
- case "get_auth_link":
696
- if (!await enhancedMS365Auth.isAuthenticated() || args?.force) {
697
- const authUrl = await enhancedMS365Auth.getAuthUrl();
698
- return {
699
- content: [
700
- {
701
- type: "text",
702
- text: `šŸ”— Microsoft 365 Authentication Link:\n\n${authUrl}\n\nClick this link to authenticate with Microsoft 365.`
703
- }
704
- ]
705
- };
536
+ // ============ UNIFIED EMAIL MANAGEMENT TOOL ============
537
+ case "manage_email":
538
+ if (ms365Config.multiUser) {
539
+ const userId = args?.userId;
540
+ if (!userId) {
541
+ throw new Error("User ID is required in multi-user mode");
542
+ }
543
+ const graphClient = await multiUserMS365Auth.getGraphClientForUser(userId);
544
+ ms365Ops.setGraphClient(graphClient);
706
545
  }
707
546
  else {
708
- return {
709
- content: [
710
- {
711
- type: "text",
712
- text: "āœ… Already authenticated with Microsoft 365. Use force: true to get a new authentication link."
713
- }
714
- ]
715
- };
547
+ const graphClient = await enhancedMS365Auth.getGraphClient();
548
+ ms365Ops.setGraphClient(graphClient);
716
549
  }
717
- case "device_code_login":
718
- if (!await enhancedMS365Auth.isAuthenticated() || args?.force) {
719
- const deviceCodeInfo = await enhancedMS365Auth.startDeviceCodeAuth();
720
- return {
721
- content: [
722
- {
723
- type: "text",
724
- text: `šŸ” Microsoft 365 Device Code Authentication Started!\n\nšŸ“± Visit: ${deviceCodeInfo.verificationUri}\nšŸ”‘ Enter this code: ${deviceCodeInfo.userCode}\n\nā³ After entering the code, use the "authenticate_with_device_code" tool to complete authentication.\n\nšŸ’” This code will expire in 15 minutes.`
550
+ const emailAction = args?.action;
551
+ switch (emailAction) {
552
+ case "read":
553
+ if (!args?.messageId) {
554
+ throw new Error("messageId is required for read action");
555
+ }
556
+ const email = await ms365Ops.getEmail(args.messageId, args?.includeAttachments);
557
+ return {
558
+ content: [
559
+ {
560
+ type: "text",
561
+ text: `šŸ“§ Email Details\n\nšŸ“‹ Subject: ${email.subject}\nšŸ‘¤ From: ${email.from.name} <${email.from.address}>\nšŸ“… Date: ${email.receivedDateTime}\nšŸ“Ž Attachments: ${email.attachments?.length || 0}\n\nšŸ’¬ Body:\n${email.body || email.bodyPreview}`
562
+ }
563
+ ]
564
+ };
565
+ case "search":
566
+ const searchResults = await ms365Ops.searchEmails(args);
567
+ // Enhanced feedback for search results
568
+ let responseText = `šŸ” Email Search Results (${searchResults.messages.length} found)`;
569
+ if (searchResults.messages.length === 0) {
570
+ responseText = `šŸ” No emails found matching your criteria.\n\nšŸ’” Search Tips:\n`;
571
+ if (args?.from) {
572
+ responseText += `• Try partial names: "${args.from.split(' ')[0]}" or "${args.from.split(' ').pop()}"\n`;
573
+ responseText += `• Check spelling of sender name\n`;
725
574
  }
726
- ]
727
- };
728
- }
729
- else {
730
- return {
731
- content: [
732
- {
733
- type: "text",
734
- text: "āœ… Already authenticated with Microsoft 365. Use force: true to re-authenticate."
575
+ if (args?.subject) {
576
+ responseText += `• Try broader subject terms\n`;
735
577
  }
736
- ]
737
- };
738
- }
739
- case "check_pending_auth":
740
- const pendingDeviceCodeState = await enhancedMS365Auth.getPendingDeviceCodeInfo();
741
- if (pendingDeviceCodeState) {
742
- return {
743
- content: [
744
- {
745
- type: "text",
746
- text: `ā³ Pending Device Code Authentication\n\nšŸ“± Visit: ${pendingDeviceCodeState.verificationUri}\nšŸ”‘ Enter this code: ${pendingDeviceCodeState.userCode}\n\nšŸ’” Use 'authenticate_with_device_code' to finish authentication after entering the code.`
578
+ if (args?.after || args?.before) {
579
+ responseText += `• Try expanding date range\n`;
747
580
  }
748
- ]
749
- };
750
- }
751
- return {
752
- content: [
753
- {
754
- type: "text",
755
- text: "ā„¹ļø No pending device code authentication found. Use 'device_code_login' to start a new authentication process."
756
- }
757
- ]
758
- };
759
- case "get_device_code":
760
- const deviceCodeInfo = await enhancedMS365Auth.getDeviceCodeInfo();
761
- return {
762
- content: [
763
- {
764
- type: "text",
765
- text: `šŸ” Microsoft 365 Device Code Authentication\n\nšŸ“± Visit: ${deviceCodeInfo.verificationUri}\nšŸ”‘ Enter code: ${deviceCodeInfo.userCode}\n\nā³ This code will expire in 15 minutes.`
766
- }
767
- ]
768
- };
769
- case "verify_authentication":
770
- const isAuthenticated = await enhancedMS365Auth.isAuthenticated();
771
- const currentUser = await enhancedMS365Auth.getCurrentUser();
772
- const storageInfo = enhancedMS365Auth.getStorageInfo();
773
- const tokenInfo = await enhancedMS365Auth.getTokenExpirationInfo();
774
- let statusText = `šŸ“Š Microsoft 365 Authentication Status\n\nšŸ” Authentication: ${isAuthenticated ? 'āœ… Valid' : 'āŒ Not authenticated'}\nšŸ‘¤ Current User: ${currentUser || 'None'}\nšŸ’¾ Storage method: ${storageInfo.method}\nšŸ“ Storage location: ${storageInfo.location}`;
775
- if (isAuthenticated) {
776
- statusText += `\nā° Token expires in: ${tokenInfo.expiresInMinutes} minutes`;
777
- if (tokenInfo.needsRefresh) {
778
- statusText += `\nāš ļø Token will be refreshed automatically on next operation`;
779
- }
780
- }
781
- else {
782
- statusText += `\nšŸ’” Use the "authenticate_with_device_code" tool to sign in`;
783
- }
784
- return {
785
- content: [
786
- {
787
- type: "text",
788
- text: statusText
789
- }
790
- ]
791
- };
792
- case "logout":
793
- const accountKey = args?.accountKey;
794
- const wasAuthenticated = await enhancedMS365Auth.isAuthenticated();
795
- await enhancedMS365Auth.resetAuth();
796
- return {
797
- content: [
798
- {
799
- type: "text",
800
- text: wasAuthenticated ?
801
- `āœ… Successfully logged out from Microsoft 365.` :
802
- `ā„¹ļø No active authentication found.`
581
+ responseText += `• Remove some search criteria to get broader results`;
803
582
  }
804
- ]
805
- };
806
- // ============ MULTI-USER AUTHENTICATION TOOLS ============
807
- case "authenticate_user":
808
- if (!ms365Config.multiUser) {
809
- throw new Error("Multi-user mode not enabled. Start server with --multi-user flag.");
810
- }
811
- const authResult = await multiUserMS365Auth.authenticateNewUser(args?.userEmail || undefined);
812
- return {
813
- content: [
814
- {
815
- type: "text",
816
- text: `šŸ”— Authentication started for user: ${authResult.userId}\n\nšŸ“± Visit this URL to authenticate:\n${authResult.authUrl}\n\nāš ļø Server running on port ${authResult.port}`
817
- }
818
- ]
819
- };
820
- case "remove_my_session":
821
- if (!ms365Config.multiUser) {
822
- throw new Error("Multi-user mode not enabled. Start server with --multi-user flag.");
823
- }
824
- if (!args?.userId) {
825
- throw new Error("User ID is required");
826
- }
827
- const removed = multiUserMS365Auth.removeUser(args.userId);
828
- return {
829
- content: [
830
- {
831
- type: "text",
832
- text: removed ? `āœ… Session removed for user: ${args.userId}` : `āŒ Session not found for user: ${args.userId}`
583
+ else {
584
+ responseText += `\n\n${searchResults.messages.map((email, index) => `${index + 1}. šŸ“§ ${email.subject}\n šŸ‘¤ From: ${email.from.name} <${email.from.address}>\n šŸ“… ${new Date(email.receivedDateTime).toLocaleDateString()}\n ${email.isRead ? 'šŸ“– Read' : 'šŸ“© Unread'}\n šŸ†” ID: ${email.id}\n`).join('\n')}`;
585
+ if (searchResults.hasMore) {
586
+ responseText += `\nšŸ’” There are more results available. Use maxResults parameter to get more emails.`;
587
+ }
833
588
  }
834
- ]
835
- };
836
- // ============ EMAIL OPERATIONS ============
837
- case "send_email":
838
- if (ms365Config.multiUser) {
839
- const userId = args?.userId;
840
- if (!userId) {
841
- throw new Error("User ID is required in multi-user mode");
842
- }
843
- const graphClient = await multiUserMS365Auth.getGraphClientForUser(userId);
844
- ms365Ops.setGraphClient(graphClient);
845
- }
846
- else {
847
- const graphClient = await enhancedMS365Auth.getGraphClient();
848
- ms365Ops.setGraphClient(graphClient);
849
- }
850
- const emailResult = await ms365Ops.sendEmail(args);
851
- return {
852
- content: [
853
- {
854
- type: "text",
855
- text: `āœ… Email sent successfully!\n\nšŸ“§ To: ${Array.isArray(args?.to) ? args.to.join(', ') : args?.to}\nšŸ“‹ Subject: ${args?.subject}\nšŸ†” Message ID: ${emailResult.id}`
589
+ return {
590
+ content: [
591
+ {
592
+ type: "text",
593
+ text: responseText
594
+ }
595
+ ]
596
+ };
597
+ case "search_to_me":
598
+ const searchToMeResults = await ms365Ops.searchEmailsToMe(args);
599
+ return {
600
+ content: [
601
+ {
602
+ type: "text",
603
+ text: `šŸ” Emails Addressed to You (TO & CC) - ${searchToMeResults.messages.length} found\n\n${searchToMeResults.messages.map((email, index) => `${index + 1}. šŸ“§ ${email.subject}\n šŸ‘¤ From: ${email.from.name} <${email.from.address}>\n šŸ“… ${new Date(email.receivedDateTime).toLocaleDateString()}\n šŸ†” ID: ${email.id}\n`).join('\n')}`
604
+ }
605
+ ]
606
+ };
607
+ case "list":
608
+ const emailList = await ms365Ops.listEmails(args?.folderId, args?.maxResults);
609
+ return {
610
+ content: [
611
+ {
612
+ type: "text",
613
+ text: `šŸ“¬ Email List (${emailList.messages.length} emails)\n\n${emailList.messages.map((email, index) => `${index + 1}. šŸ“§ ${email.subject}\n šŸ‘¤ From: ${email.from.name} <${email.from.address}>\n šŸ“… ${new Date(email.receivedDateTime).toLocaleDateString()}\n ${email.isRead ? 'šŸ“–' : 'šŸ“©'} ${email.isRead ? 'Read' : 'Unread'}\n šŸ†” ID: ${email.id}\n`).join('\n')}`
614
+ }
615
+ ]
616
+ };
617
+ case "mark":
618
+ if (!args?.messageId || args?.isRead === undefined) {
619
+ throw new Error("messageId and isRead are required for mark action");
856
620
  }
857
- ]
858
- };
859
- case "read_email":
860
- if (ms365Config.multiUser) {
861
- const userId = args?.userId;
862
- if (!userId) {
863
- throw new Error("User ID is required in multi-user mode");
864
- }
865
- const graphClient = await multiUserMS365Auth.getGraphClientForUser(userId);
866
- ms365Ops.setGraphClient(graphClient);
867
- }
868
- else {
869
- const graphClient = await enhancedMS365Auth.getGraphClient();
870
- ms365Ops.setGraphClient(graphClient);
871
- }
872
- const email = await ms365Ops.getEmail(args?.messageId, args?.includeAttachments);
873
- return {
874
- content: [
875
- {
876
- type: "text",
877
- text: `šŸ“§ Email Details\n\nšŸ“‹ Subject: ${email.subject}\nšŸ‘¤ From: ${email.from.name} <${email.from.address}>\nšŸ“… Date: ${email.receivedDateTime}\nšŸ“Ž Attachments: ${email.attachments?.length || 0}\n\nšŸ’¬ Body:\n${email.body || email.bodyPreview}`
621
+ await ms365Ops.markEmail(args.messageId, args.isRead);
622
+ return {
623
+ content: [
624
+ {
625
+ type: "text",
626
+ text: `āœ… Email marked as ${args.isRead ? 'read' : 'unread'}\nšŸ†” Message ID: ${args.messageId}`
627
+ }
628
+ ]
629
+ };
630
+ case "move":
631
+ if (!args?.messageId || !args?.destinationFolderId) {
632
+ throw new Error("messageId and destinationFolderId are required for move action");
878
633
  }
879
- ]
880
- };
881
- case "search_emails":
882
- if (ms365Config.multiUser) {
883
- const userId = args?.userId;
884
- if (!userId) {
885
- throw new Error("User ID is required in multi-user mode");
886
- }
887
- const graphClient = await multiUserMS365Auth.getGraphClientForUser(userId);
888
- ms365Ops.setGraphClient(graphClient);
889
- }
890
- else {
891
- const graphClient = await enhancedMS365Auth.getGraphClient();
892
- ms365Ops.setGraphClient(graphClient);
893
- }
894
- const searchResults = await ms365Ops.searchEmails(args);
895
- // Enhanced feedback for search results
896
- let responseText = `šŸ” Email Search Results (${searchResults.messages.length} found)`;
897
- if (searchResults.messages.length === 0) {
898
- responseText = `šŸ” No emails found matching your criteria.\n\nšŸ’” Search Tips:\n`;
899
- if (args?.from) {
900
- responseText += `• Try partial names: "${args.from.split(' ')[0]}" or "${args.from.split(' ').pop()}"\n`;
901
- responseText += `• Check spelling of sender name\n`;
902
- }
903
- if (args?.subject) {
904
- responseText += `• Try broader subject terms\n`;
905
- }
906
- if (args?.after || args?.before) {
907
- responseText += `• Try expanding date range\n`;
908
- }
909
- responseText += `• Remove some search criteria to get broader results`;
910
- }
911
- else {
912
- responseText += `\n\n${searchResults.messages.map((email, index) => `${index + 1}. šŸ“§ ${email.subject}\n šŸ‘¤ From: ${email.from.name} <${email.from.address}>\n šŸ“… ${new Date(email.receivedDateTime).toLocaleDateString()}\n ${email.isRead ? 'šŸ“– Read' : 'šŸ“© Unread'}\n šŸ†” ID: ${email.id}\n`).join('\n')}`;
913
- if (searchResults.hasMore) {
914
- responseText += `\nšŸ’” There are more results available. Use maxResults parameter to get more emails.`;
915
- }
916
- }
917
- return {
918
- content: [
919
- {
920
- type: "text",
921
- text: responseText
634
+ await ms365Ops.moveEmail(args.messageId, args.destinationFolderId);
635
+ return {
636
+ content: [
637
+ {
638
+ type: "text",
639
+ text: `āœ… Email moved to folder: ${args.destinationFolderId}\nšŸ†” Message ID: ${args.messageId}`
640
+ }
641
+ ]
642
+ };
643
+ case "delete":
644
+ if (!args?.messageId) {
645
+ throw new Error("messageId is required for delete action");
922
646
  }
923
- ]
924
- };
925
- case "search_emails_to_me":
926
- if (ms365Config.multiUser) {
927
- const userId = args?.userId;
928
- if (!userId) {
929
- throw new Error("User ID is required in multi-user mode");
930
- }
931
- const graphClient = await multiUserMS365Auth.getGraphClientForUser(userId);
932
- ms365Ops.setGraphClient(graphClient);
933
- }
934
- else {
935
- const graphClient = await enhancedMS365Auth.getGraphClient();
936
- ms365Ops.setGraphClient(graphClient);
647
+ await ms365Ops.deleteEmail(args.messageId);
648
+ return {
649
+ content: [
650
+ {
651
+ type: "text",
652
+ text: `āœ… Email deleted permanently\nšŸ†” Message ID: ${args.messageId}`
653
+ }
654
+ ]
655
+ };
656
+ default:
657
+ throw new Error(`Unknown email action: ${emailAction}`);
937
658
  }
938
- const searchToMeResults = await ms365Ops.searchEmailsToMe(args);
939
- return {
940
- content: [
941
- {
942
- type: "text",
943
- text: `šŸ” Emails Addressed to You (TO & CC) - ${searchToMeResults.messages.length} found\n\n${searchToMeResults.messages.map((email, index) => `${index + 1}. šŸ“§ ${email.subject}\n šŸ‘¤ From: ${email.from.name} <${email.from.address}>\n šŸ“… ${new Date(email.receivedDateTime).toLocaleDateString()}\n šŸ†” ID: ${email.id}\n`).join('\n')}`
944
- }
945
- ]
946
- };
947
- case "mark_email":
659
+ // ============ UNIFIED CONTACT MANAGEMENT TOOL ============
660
+ case "manage_contacts":
948
661
  if (ms365Config.multiUser) {
949
662
  const userId = args?.userId;
950
663
  if (!userId) {
@@ -957,38 +670,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
957
670
  const graphClient = await enhancedMS365Auth.getGraphClient();
958
671
  ms365Ops.setGraphClient(graphClient);
959
672
  }
960
- await ms365Ops.markEmail(args?.messageId, args?.isRead);
961
- return {
962
- content: [
963
- {
964
- type: "text",
965
- text: `āœ… Email marked as ${args?.isRead ? 'read' : 'unread'}\nšŸ†” Message ID: ${args?.messageId}`
673
+ const contactAction = args?.action || 'list';
674
+ switch (contactAction) {
675
+ case "list":
676
+ const contacts = await ms365Ops.getContacts(args?.maxResults || 100);
677
+ return {
678
+ content: [
679
+ {
680
+ type: "text",
681
+ text: `šŸ‘„ Contacts (${contacts.length} found)\n\n${contacts.map((contact) => `šŸ‘¤ ${contact.displayName}\n šŸ“§ ${contact.emailAddresses?.[0]?.address || 'No email'}\n šŸ“ž ${contact.businessPhones?.[0] || 'No phone'}\n`).join('\n')}`
682
+ }
683
+ ]
684
+ };
685
+ case "search":
686
+ if (!args?.query) {
687
+ throw new Error("query is required for search action");
966
688
  }
967
- ]
968
- };
969
- case "move_email":
970
- if (ms365Config.multiUser) {
971
- const userId = args?.userId;
972
- if (!userId) {
973
- throw new Error("User ID is required in multi-user mode");
974
- }
975
- const graphClient = await multiUserMS365Auth.getGraphClientForUser(userId);
976
- ms365Ops.setGraphClient(graphClient);
977
- }
978
- else {
979
- const graphClient = await enhancedMS365Auth.getGraphClient();
980
- ms365Ops.setGraphClient(graphClient);
689
+ const searchContactResults = await ms365Ops.searchContacts(args.query, args?.maxResults || 50);
690
+ return {
691
+ content: [
692
+ {
693
+ type: "text",
694
+ text: `šŸ” Contact Search Results (${searchContactResults.length} found)\n\n${searchContactResults.map((contact) => `šŸ‘¤ ${contact.displayName}\n šŸ“§ ${contact.emailAddresses?.[0]?.address || 'No email'}\n šŸ“ž ${contact.businessPhones?.[0] || 'No phone'}\n`).join('\n')}`
695
+ }
696
+ ]
697
+ };
698
+ default:
699
+ throw new Error(`Unknown contact action: ${contactAction}`);
981
700
  }
982
- await ms365Ops.moveEmail(args?.messageId, args?.destinationFolderId);
983
- return {
984
- content: [
985
- {
986
- type: "text",
987
- text: `āœ… Email moved to folder: ${args?.destinationFolderId}\nšŸ†” Message ID: ${args?.messageId}`
988
- }
989
- ]
990
- };
991
- case "delete_email":
701
+ // ============ REMAINING ORIGINAL TOOLS ============
702
+ case "send_email":
992
703
  if (ms365Config.multiUser) {
993
704
  const userId = args?.userId;
994
705
  if (!userId) {
@@ -1001,12 +712,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1001
712
  const graphClient = await enhancedMS365Auth.getGraphClient();
1002
713
  ms365Ops.setGraphClient(graphClient);
1003
714
  }
1004
- await ms365Ops.deleteEmail(args?.messageId);
715
+ const emailResult = await ms365Ops.sendEmail(args);
1005
716
  return {
1006
717
  content: [
1007
718
  {
1008
719
  type: "text",
1009
- text: `āœ… Email deleted permanently\nšŸ†” Message ID: ${args?.messageId}`
720
+ text: `āœ… Email sent successfully!\n\nšŸ“§ To: ${Array.isArray(args?.to) ? args.to.join(', ') : args?.to}\nšŸ“‹ Subject: ${args?.subject}\nšŸ†” Message ID: ${emailResult.id}`
1010
721
  }
1011
722
  ]
1012
723
  };
@@ -1054,69 +765,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1054
765
  }
1055
766
  ]
1056
767
  };
1057
- case "get_contacts":
1058
- if (ms365Config.multiUser) {
1059
- const userId = args?.userId;
1060
- if (!userId) {
1061
- throw new Error("User ID is required in multi-user mode");
1062
- }
1063
- const graphClient = await multiUserMS365Auth.getGraphClientForUser(userId);
1064
- ms365Ops.setGraphClient(graphClient);
1065
- }
1066
- else {
1067
- const graphClient = await enhancedMS365Auth.getGraphClient();
1068
- ms365Ops.setGraphClient(graphClient);
1069
- }
1070
- const contacts = await ms365Ops.getContacts(args?.maxResults);
1071
- return {
1072
- content: [
1073
- {
1074
- type: "text",
1075
- text: `šŸ‘„ Contacts (${contacts.length} found)\n\n${contacts.map((contact) => `šŸ‘¤ ${contact.displayName}\n šŸ“§ ${contact.emailAddresses?.[0]?.address || 'No email'}\n šŸ“ž ${contact.businessPhones?.[0] || 'No phone'}\n`).join('\n')}`
1076
- }
1077
- ]
1078
- };
1079
- case "search_contacts":
1080
- if (ms365Config.multiUser) {
1081
- const userId = args?.userId;
1082
- if (!userId) {
1083
- throw new Error("User ID is required in multi-user mode");
1084
- }
1085
- const graphClient = await multiUserMS365Auth.getGraphClientForUser(userId);
1086
- ms365Ops.setGraphClient(graphClient);
1087
- }
1088
- else {
1089
- const graphClient = await enhancedMS365Auth.getGraphClient();
1090
- ms365Ops.setGraphClient(graphClient);
768
+ // ============ MULTI-USER AUTHENTICATION TOOLS ============
769
+ case "authenticate_user":
770
+ if (!ms365Config.multiUser) {
771
+ throw new Error("Multi-user mode not enabled. Start server with --multi-user flag.");
1091
772
  }
1092
- const searchContactResults = await ms365Ops.searchContacts(args?.query, args?.maxResults);
773
+ const authResult = await multiUserMS365Auth.authenticateNewUser(args?.userEmail || undefined);
1093
774
  return {
1094
775
  content: [
1095
776
  {
1096
777
  type: "text",
1097
- text: `šŸ” Contact Search Results (${searchContactResults.length} found)\n\n${searchContactResults.map((contact) => `šŸ‘¤ ${contact.displayName}\n šŸ“§ ${contact.emailAddresses?.[0]?.address || 'No email'}\n šŸ“ž ${contact.businessPhones?.[0] || 'No phone'}\n`).join('\n')}`
778
+ text: `šŸ”— Authentication started for user: ${authResult.userId}\n\nšŸ“± Visit this URL to authenticate:\n${authResult.authUrl}\n\nāš ļø Server running on port ${authResult.port}`
1098
779
  }
1099
780
  ]
1100
781
  };
1101
- case "list_emails":
1102
- if (ms365Config.multiUser) {
1103
- const userId = args?.userId;
1104
- if (!userId) {
1105
- throw new Error("User ID is required in multi-user mode");
1106
- }
1107
- const graphClient = await multiUserMS365Auth.getGraphClientForUser(userId);
1108
- ms365Ops.setGraphClient(graphClient);
782
+ case "remove_my_session":
783
+ if (!ms365Config.multiUser) {
784
+ throw new Error("Multi-user mode not enabled. Start server with --multi-user flag.");
1109
785
  }
1110
- else {
1111
- const graphClient = await enhancedMS365Auth.getGraphClient();
1112
- ms365Ops.setGraphClient(graphClient);
786
+ if (!args?.userId) {
787
+ throw new Error("User ID is required");
1113
788
  }
1114
- const emailList = await ms365Ops.listEmails(args?.folderId, args?.maxResults);
789
+ const removed = multiUserMS365Auth.removeUser(args.userId);
1115
790
  return {
1116
791
  content: [
1117
792
  {
1118
793
  type: "text",
1119
- text: `šŸ“¬ Email List (${emailList.messages.length} emails)\n\n${emailList.messages.map((email, index) => `${index + 1}. šŸ“§ ${email.subject}\n šŸ‘¤ From: ${email.from.name} <${email.from.address}>\n šŸ“… ${new Date(email.receivedDateTime).toLocaleDateString()}\n ${email.isRead ? 'šŸ“–' : 'šŸ“©'} ${email.isRead ? 'Read' : 'Unread'}\n šŸ†” ID: ${email.id}\n`).join('\n')}`
794
+ text: removed ? `āœ… Session removed for user: ${args.userId}` : `āŒ Session not found for user: ${args.userId}`
1120
795
  }
1121
796
  ]
1122
797
  };