ms365-mcp-server 1.0.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.
@@ -0,0 +1,644 @@
1
+ import { ms365Auth } from './ms365-auth.js';
2
+ import { logger } from './api.js';
3
+ /**
4
+ * Microsoft 365 operations manager class
5
+ */
6
+ export class MS365Operations {
7
+ constructor() {
8
+ this.graphClient = null;
9
+ }
10
+ /**
11
+ * Set the Microsoft Graph client externally
12
+ */
13
+ setGraphClient(client) {
14
+ this.graphClient = client;
15
+ }
16
+ /**
17
+ * Get authenticated Microsoft Graph client
18
+ */
19
+ async getGraphClient() {
20
+ if (!this.graphClient) {
21
+ this.graphClient = await ms365Auth.getGraphClient();
22
+ }
23
+ return this.graphClient;
24
+ }
25
+ /**
26
+ * Build filter query for Microsoft Graph API
27
+ */
28
+ buildFilterQuery(criteria) {
29
+ const filters = [];
30
+ if (criteria.from) {
31
+ filters.push(`from/emailAddress/address eq '${criteria.from}'`);
32
+ }
33
+ // Note: Cannot filter on toRecipients or ccRecipients as they are complex collections
34
+ // These will be handled by search API or manual filtering
35
+ if (criteria.subject) {
36
+ filters.push(`contains(subject,'${criteria.subject}')`);
37
+ }
38
+ if (criteria.after) {
39
+ const afterDate = new Date(criteria.after).toISOString();
40
+ filters.push(`receivedDateTime ge ${afterDate}`);
41
+ }
42
+ if (criteria.before) {
43
+ const beforeDate = new Date(criteria.before).toISOString();
44
+ filters.push(`receivedDateTime le ${beforeDate}`);
45
+ }
46
+ if (criteria.hasAttachment !== undefined) {
47
+ filters.push(`hasAttachments eq ${criteria.hasAttachment}`);
48
+ }
49
+ if (criteria.isUnread !== undefined) {
50
+ filters.push(`isRead eq ${!criteria.isUnread}`);
51
+ }
52
+ if (criteria.importance) {
53
+ filters.push(`importance eq '${criteria.importance}'`);
54
+ }
55
+ return filters.join(' and ');
56
+ }
57
+ /**
58
+ * Build search query for Microsoft Graph API
59
+ */
60
+ buildSearchQuery(criteria) {
61
+ const searchTerms = [];
62
+ if (criteria.query) {
63
+ searchTerms.push(criteria.query);
64
+ }
65
+ // Build email-specific search terms using proper Microsoft Graph syntax
66
+ const emailSearchTerms = [];
67
+ if (criteria.from) {
68
+ emailSearchTerms.push(`from:${criteria.from}`);
69
+ }
70
+ if (criteria.to) {
71
+ emailSearchTerms.push(`to:${criteria.to}`);
72
+ }
73
+ if (criteria.cc) {
74
+ emailSearchTerms.push(`cc:${criteria.cc}`);
75
+ }
76
+ // Combine email search terms with OR logic
77
+ if (emailSearchTerms.length > 0) {
78
+ searchTerms.push(emailSearchTerms.join(' OR '));
79
+ }
80
+ if (criteria.subject) {
81
+ // For subject searches, we can use quotes if the subject contains spaces
82
+ if (criteria.subject.includes(' ')) {
83
+ searchTerms.push(`subject:"${criteria.subject}"`);
84
+ }
85
+ else {
86
+ searchTerms.push(`subject:${criteria.subject}`);
87
+ }
88
+ }
89
+ return searchTerms.join(' AND ');
90
+ }
91
+ /**
92
+ * Send an email
93
+ */
94
+ async sendEmail(message) {
95
+ try {
96
+ const graphClient = await this.getGraphClient();
97
+ // Prepare recipients
98
+ const toRecipients = message.to.map(email => ({
99
+ emailAddress: {
100
+ address: email,
101
+ name: email.split('@')[0]
102
+ }
103
+ }));
104
+ const ccRecipients = message.cc?.map(email => ({
105
+ emailAddress: {
106
+ address: email,
107
+ name: email.split('@')[0]
108
+ }
109
+ })) || [];
110
+ const bccRecipients = message.bcc?.map(email => ({
111
+ emailAddress: {
112
+ address: email,
113
+ name: email.split('@')[0]
114
+ }
115
+ })) || [];
116
+ // Prepare attachments
117
+ const attachments = message.attachments?.map(att => ({
118
+ '@odata.type': '#microsoft.graph.fileAttachment',
119
+ name: att.name,
120
+ contentBytes: att.contentBytes,
121
+ contentType: att.contentType || 'application/octet-stream'
122
+ })) || [];
123
+ // Prepare email body
124
+ const emailBody = {
125
+ subject: message.subject,
126
+ body: {
127
+ contentType: message.bodyType === 'html' ? 'html' : 'text',
128
+ content: message.body || ''
129
+ },
130
+ toRecipients,
131
+ ccRecipients,
132
+ bccRecipients,
133
+ importance: message.importance || 'normal',
134
+ attachments: attachments.length > 0 ? attachments : undefined
135
+ };
136
+ if (message.replyTo) {
137
+ emailBody.replyTo = [{
138
+ emailAddress: {
139
+ address: message.replyTo,
140
+ name: message.replyTo.split('@')[0]
141
+ }
142
+ }];
143
+ }
144
+ // Send email
145
+ const result = await graphClient
146
+ .api('/me/sendMail')
147
+ .post({
148
+ message: emailBody
149
+ });
150
+ logger.log('Email sent successfully');
151
+ return {
152
+ id: result?.id || 'sent',
153
+ status: 'sent'
154
+ };
155
+ }
156
+ catch (error) {
157
+ logger.error('Error sending email:', error);
158
+ throw error;
159
+ }
160
+ }
161
+ /**
162
+ * Get email by ID
163
+ */
164
+ async getEmail(messageId, includeAttachments = false) {
165
+ try {
166
+ const graphClient = await this.getGraphClient();
167
+ let selectFields = 'id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink';
168
+ if (includeAttachments) {
169
+ selectFields += ',body';
170
+ }
171
+ const email = await graphClient
172
+ .api(`/me/messages/${messageId}`)
173
+ .select(selectFields)
174
+ .get();
175
+ const emailInfo = {
176
+ id: email.id,
177
+ subject: email.subject || '',
178
+ from: {
179
+ name: email.from?.emailAddress?.name || '',
180
+ address: email.from?.emailAddress?.address || ''
181
+ },
182
+ toRecipients: email.toRecipients?.map((recipient) => ({
183
+ name: recipient.emailAddress?.name || '',
184
+ address: recipient.emailAddress?.address || ''
185
+ })) || [],
186
+ ccRecipients: email.ccRecipients?.map((recipient) => ({
187
+ name: recipient.emailAddress?.name || '',
188
+ address: recipient.emailAddress?.address || ''
189
+ })) || [],
190
+ receivedDateTime: email.receivedDateTime,
191
+ sentDateTime: email.sentDateTime,
192
+ bodyPreview: email.bodyPreview || '',
193
+ isRead: email.isRead || false,
194
+ hasAttachments: email.hasAttachments || false,
195
+ importance: email.importance || 'normal',
196
+ conversationId: email.conversationId || '',
197
+ parentFolderId: email.parentFolderId || '',
198
+ webLink: email.webLink || ''
199
+ };
200
+ if (includeAttachments && email.body) {
201
+ emailInfo.body = email.body.content || '';
202
+ }
203
+ // Get attachments if requested
204
+ if (includeAttachments && email.hasAttachments) {
205
+ const attachments = await graphClient
206
+ .api(`/me/messages/${messageId}/attachments`)
207
+ .get();
208
+ emailInfo.attachments = attachments.value || [];
209
+ }
210
+ return emailInfo;
211
+ }
212
+ catch (error) {
213
+ logger.error('Error getting email:', error);
214
+ throw error;
215
+ }
216
+ }
217
+ /**
218
+ * Search emails with criteria
219
+ */
220
+ async searchEmails(criteria = {}) {
221
+ try {
222
+ const graphClient = await this.getGraphClient();
223
+ // Build search and filter queries
224
+ const searchQuery = this.buildSearchQuery(criteria);
225
+ const filterQuery = this.buildFilterQuery(criteria);
226
+ let apiCall;
227
+ let useSearchAPI = !!searchQuery;
228
+ try {
229
+ // Microsoft Graph doesn't support $orderBy with $search, so we need to choose one approach
230
+ if (searchQuery) {
231
+ // Use search API when we have search terms (no orderby allowed)
232
+ // Add ConsistencyLevel header for search queries
233
+ apiCall = graphClient.api('/me/messages')
234
+ .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
235
+ .header('ConsistencyLevel', 'eventual')
236
+ .search(`"${searchQuery}"`)
237
+ .top(criteria.maxResults || 50);
238
+ // Can't use filter with search, so we'll need to filter results manually if needed
239
+ }
240
+ else {
241
+ // Use filter API when we don't have search terms (orderby allowed)
242
+ apiCall = graphClient.api('/me/messages')
243
+ .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
244
+ .orderby('receivedDateTime desc')
245
+ .top(criteria.maxResults || 50);
246
+ if (filterQuery) {
247
+ apiCall = apiCall.filter(filterQuery);
248
+ }
249
+ }
250
+ // Handle folder-specific searches
251
+ if (criteria.folder && criteria.folder !== 'inbox') {
252
+ if (searchQuery) {
253
+ // For folder + search, we need to use a different approach
254
+ apiCall = graphClient.api(`/me/mailFolders/${criteria.folder}/messages`)
255
+ .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
256
+ .top(criteria.maxResults || 50);
257
+ if (filterQuery) {
258
+ apiCall = apiCall.filter(filterQuery);
259
+ }
260
+ // Note: Folder-specific search is limited, we'll do text filtering on results
261
+ }
262
+ else {
263
+ apiCall = graphClient.api(`/me/mailFolders/${criteria.folder}/messages`)
264
+ .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
265
+ .orderby('receivedDateTime desc')
266
+ .top(criteria.maxResults || 50);
267
+ if (filterQuery) {
268
+ apiCall = apiCall.filter(filterQuery);
269
+ }
270
+ }
271
+ }
272
+ const result = await apiCall.get();
273
+ let messages = result.value?.map((email) => ({
274
+ id: email.id,
275
+ subject: email.subject || '',
276
+ from: {
277
+ name: email.from?.emailAddress?.name || '',
278
+ address: email.from?.emailAddress?.address || ''
279
+ },
280
+ toRecipients: email.toRecipients?.map((recipient) => ({
281
+ name: recipient.emailAddress?.name || '',
282
+ address: recipient.emailAddress?.address || ''
283
+ })) || [],
284
+ ccRecipients: email.ccRecipients?.map((recipient) => ({
285
+ name: recipient.emailAddress?.name || '',
286
+ address: recipient.emailAddress?.address || ''
287
+ })) || [],
288
+ receivedDateTime: email.receivedDateTime,
289
+ sentDateTime: email.sentDateTime,
290
+ bodyPreview: email.bodyPreview || '',
291
+ isRead: email.isRead || false,
292
+ hasAttachments: email.hasAttachments || false,
293
+ importance: email.importance || 'normal',
294
+ conversationId: email.conversationId || '',
295
+ parentFolderId: email.parentFolderId || '',
296
+ webLink: email.webLink || ''
297
+ })) || [];
298
+ // Apply manual filtering when using search (since we can't use $filter with $search)
299
+ if (useSearchAPI && (criteria.folder || filterQuery)) {
300
+ messages = this.applyManualFiltering(messages, criteria);
301
+ }
302
+ return {
303
+ messages,
304
+ hasMore: !!result['@odata.nextLink']
305
+ };
306
+ }
307
+ catch (searchError) {
308
+ // If search API fails due to syntax error, fall back to filter-only approach
309
+ if (searchError.message && searchError.message.includes('Syntax error')) {
310
+ logger.log('Search API failed with syntax error, falling back to filter-only query');
311
+ // Fallback: Use only filter-based query without search
312
+ apiCall = graphClient.api('/me/messages')
313
+ .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
314
+ .orderby('receivedDateTime desc')
315
+ .top(criteria.maxResults || 50);
316
+ if (filterQuery) {
317
+ apiCall = apiCall.filter(filterQuery);
318
+ }
319
+ // Handle folder-specific queries
320
+ if (criteria.folder && criteria.folder !== 'inbox') {
321
+ apiCall = graphClient.api(`/me/mailFolders/${criteria.folder}/messages`)
322
+ .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
323
+ .orderby('receivedDateTime desc')
324
+ .top(criteria.maxResults || 50);
325
+ if (filterQuery) {
326
+ apiCall = apiCall.filter(filterQuery);
327
+ }
328
+ }
329
+ const result = await apiCall.get();
330
+ let messages = result.value?.map((email) => ({
331
+ id: email.id,
332
+ subject: email.subject || '',
333
+ from: {
334
+ name: email.from?.emailAddress?.name || '',
335
+ address: email.from?.emailAddress?.address || ''
336
+ },
337
+ toRecipients: email.toRecipients?.map((recipient) => ({
338
+ name: recipient.emailAddress?.name || '',
339
+ address: recipient.emailAddress?.address || ''
340
+ })) || [],
341
+ ccRecipients: email.ccRecipients?.map((recipient) => ({
342
+ name: recipient.emailAddress?.name || '',
343
+ address: recipient.emailAddress?.address || ''
344
+ })) || [],
345
+ receivedDateTime: email.receivedDateTime,
346
+ sentDateTime: email.sentDateTime,
347
+ bodyPreview: email.bodyPreview || '',
348
+ isRead: email.isRead || false,
349
+ hasAttachments: email.hasAttachments || false,
350
+ importance: email.importance || 'normal',
351
+ conversationId: email.conversationId || '',
352
+ parentFolderId: email.parentFolderId || '',
353
+ webLink: email.webLink || ''
354
+ })) || [];
355
+ // Apply manual filtering for any criteria that couldn't be handled by the filter
356
+ messages = this.applyManualFiltering(messages, criteria);
357
+ return {
358
+ messages,
359
+ hasMore: !!result['@odata.nextLink']
360
+ };
361
+ }
362
+ else {
363
+ // Re-throw other errors
364
+ throw searchError;
365
+ }
366
+ }
367
+ }
368
+ catch (error) {
369
+ logger.error('Error searching emails:', error);
370
+ throw error;
371
+ }
372
+ }
373
+ /**
374
+ * Apply manual filtering to search results (used when $filter can't be used with $search)
375
+ */
376
+ applyManualFiltering(messages, criteria) {
377
+ return messages.filter(message => {
378
+ // Apply text search filters manually
379
+ if (criteria.query) {
380
+ const searchText = criteria.query.toLowerCase();
381
+ const messageText = `${message.subject} ${message.bodyPreview} ${message.from.name} ${message.from.address}`.toLowerCase();
382
+ if (!messageText.includes(searchText))
383
+ return false;
384
+ }
385
+ if (criteria.from) {
386
+ const fromMatch = message.from.address.toLowerCase().includes(criteria.from.toLowerCase()) ||
387
+ message.from.name.toLowerCase().includes(criteria.from.toLowerCase());
388
+ if (!fromMatch)
389
+ return false;
390
+ }
391
+ if (criteria.to) {
392
+ const toMatch = message.toRecipients.some(recipient => recipient.address.toLowerCase().includes(criteria.to.toLowerCase()) ||
393
+ recipient.name.toLowerCase().includes(criteria.to.toLowerCase()));
394
+ if (!toMatch)
395
+ return false;
396
+ }
397
+ if (criteria.cc) {
398
+ // Handle case where ccRecipients might be undefined
399
+ const ccMatch = message.ccRecipients && message.ccRecipients.length > 0 &&
400
+ message.ccRecipients.some(recipient => recipient.address.toLowerCase().includes(criteria.cc.toLowerCase()) ||
401
+ recipient.name.toLowerCase().includes(criteria.cc.toLowerCase()));
402
+ if (!ccMatch)
403
+ return false;
404
+ }
405
+ if (criteria.subject) {
406
+ if (!message.subject.toLowerCase().includes(criteria.subject.toLowerCase()))
407
+ return false;
408
+ }
409
+ // Apply date filters
410
+ if (criteria.after) {
411
+ const afterDate = new Date(criteria.after);
412
+ const messageDate = new Date(message.receivedDateTime);
413
+ if (messageDate < afterDate)
414
+ return false;
415
+ }
416
+ if (criteria.before) {
417
+ const beforeDate = new Date(criteria.before);
418
+ const messageDate = new Date(message.receivedDateTime);
419
+ if (messageDate > beforeDate)
420
+ return false;
421
+ }
422
+ // Apply attachment filter
423
+ if (criteria.hasAttachment !== undefined && message.hasAttachments !== criteria.hasAttachment) {
424
+ return false;
425
+ }
426
+ // Apply read status filter
427
+ if (criteria.isUnread !== undefined && message.isRead === criteria.isUnread) {
428
+ return false;
429
+ }
430
+ // Apply importance filter
431
+ if (criteria.importance && message.importance !== criteria.importance) {
432
+ return false;
433
+ }
434
+ return true;
435
+ });
436
+ }
437
+ /**
438
+ * List emails in a folder
439
+ */
440
+ async listEmails(folderId = 'inbox', maxResults = 50) {
441
+ try {
442
+ const graphClient = await this.getGraphClient();
443
+ const result = await graphClient
444
+ .api(`/me/mailFolders/${folderId}/messages`)
445
+ .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
446
+ .orderby('receivedDateTime desc')
447
+ .top(maxResults)
448
+ .get();
449
+ const messages = result.value?.map((email) => ({
450
+ id: email.id,
451
+ subject: email.subject || '',
452
+ from: {
453
+ name: email.from?.emailAddress?.name || '',
454
+ address: email.from?.emailAddress?.address || ''
455
+ },
456
+ toRecipients: email.toRecipients?.map((recipient) => ({
457
+ name: recipient.emailAddress?.name || '',
458
+ address: recipient.emailAddress?.address || ''
459
+ })) || [],
460
+ ccRecipients: email.ccRecipients?.map((recipient) => ({
461
+ name: recipient.emailAddress?.name || '',
462
+ address: recipient.emailAddress?.address || ''
463
+ })) || [],
464
+ receivedDateTime: email.receivedDateTime,
465
+ sentDateTime: email.sentDateTime,
466
+ bodyPreview: email.bodyPreview || '',
467
+ isRead: email.isRead || false,
468
+ hasAttachments: email.hasAttachments || false,
469
+ importance: email.importance || 'normal',
470
+ conversationId: email.conversationId || '',
471
+ parentFolderId: email.parentFolderId || '',
472
+ webLink: email.webLink || ''
473
+ })) || [];
474
+ return {
475
+ messages,
476
+ hasMore: !!result['@odata.nextLink']
477
+ };
478
+ }
479
+ catch (error) {
480
+ logger.error('Error listing emails:', error);
481
+ throw error;
482
+ }
483
+ }
484
+ /**
485
+ * Mark email as read or unread
486
+ */
487
+ async markEmail(messageId, isRead) {
488
+ try {
489
+ const graphClient = await this.getGraphClient();
490
+ await graphClient
491
+ .api(`/me/messages/${messageId}`)
492
+ .patch({
493
+ isRead: isRead
494
+ });
495
+ logger.log(`Email ${messageId} marked as ${isRead ? 'read' : 'unread'}`);
496
+ }
497
+ catch (error) {
498
+ logger.error('Error marking email:', error);
499
+ throw error;
500
+ }
501
+ }
502
+ /**
503
+ * Move email to folder
504
+ */
505
+ async moveEmail(messageId, destinationFolderId) {
506
+ try {
507
+ const graphClient = await this.getGraphClient();
508
+ await graphClient
509
+ .api(`/me/messages/${messageId}/move`)
510
+ .post({
511
+ destinationId: destinationFolderId
512
+ });
513
+ logger.log(`Email ${messageId} moved to folder ${destinationFolderId}`);
514
+ }
515
+ catch (error) {
516
+ logger.error('Error moving email:', error);
517
+ throw error;
518
+ }
519
+ }
520
+ /**
521
+ * Delete email
522
+ */
523
+ async deleteEmail(messageId) {
524
+ try {
525
+ const graphClient = await this.getGraphClient();
526
+ await graphClient
527
+ .api(`/me/messages/${messageId}`)
528
+ .delete();
529
+ logger.log(`Email ${messageId} deleted`);
530
+ }
531
+ catch (error) {
532
+ logger.error('Error deleting email:', error);
533
+ throw error;
534
+ }
535
+ }
536
+ /**
537
+ * Get attachment
538
+ */
539
+ async getAttachment(messageId, attachmentId) {
540
+ try {
541
+ const graphClient = await this.getGraphClient();
542
+ const attachment = await graphClient
543
+ .api(`/me/messages/${messageId}/attachments/${attachmentId}`)
544
+ .get();
545
+ return {
546
+ name: attachment.name || '',
547
+ contentType: attachment.contentType || 'application/octet-stream',
548
+ contentBytes: attachment.contentBytes || '',
549
+ size: attachment.size || 0
550
+ };
551
+ }
552
+ catch (error) {
553
+ logger.error('Error getting attachment:', error);
554
+ throw error;
555
+ }
556
+ }
557
+ /**
558
+ * List mail folders
559
+ */
560
+ async listFolders() {
561
+ try {
562
+ const graphClient = await this.getGraphClient();
563
+ const result = await graphClient
564
+ .api('/me/mailFolders')
565
+ .select('id,displayName,totalItemCount,unreadItemCount,parentFolderId')
566
+ .get();
567
+ return result.value?.map((folder) => ({
568
+ id: folder.id,
569
+ displayName: folder.displayName || '',
570
+ totalItemCount: folder.totalItemCount || 0,
571
+ unreadItemCount: folder.unreadItemCount || 0,
572
+ parentFolderId: folder.parentFolderId
573
+ })) || [];
574
+ }
575
+ catch (error) {
576
+ logger.error('Error listing folders:', error);
577
+ throw error;
578
+ }
579
+ }
580
+ /**
581
+ * Get contacts
582
+ */
583
+ async getContacts(maxResults = 100) {
584
+ try {
585
+ const graphClient = await this.getGraphClient();
586
+ const result = await graphClient
587
+ .api('/me/contacts')
588
+ .select('id,displayName,emailAddresses,jobTitle,companyName,department,officeLocation,businessPhones,mobilePhone')
589
+ .top(maxResults)
590
+ .get();
591
+ return result.value?.map((contact) => ({
592
+ id: contact.id,
593
+ displayName: contact.displayName || '',
594
+ emailAddresses: contact.emailAddresses?.map((email) => ({
595
+ name: email.name,
596
+ address: email.address
597
+ })) || [],
598
+ jobTitle: contact.jobTitle,
599
+ companyName: contact.companyName,
600
+ department: contact.department,
601
+ officeLocation: contact.officeLocation,
602
+ businessPhones: contact.businessPhones || [],
603
+ mobilePhone: contact.mobilePhone
604
+ })) || [];
605
+ }
606
+ catch (error) {
607
+ logger.error('Error getting contacts:', error);
608
+ throw error;
609
+ }
610
+ }
611
+ /**
612
+ * Search contacts
613
+ */
614
+ async searchContacts(query, maxResults = 50) {
615
+ try {
616
+ const graphClient = await this.getGraphClient();
617
+ const result = await graphClient
618
+ .api('/me/contacts')
619
+ .select('id,displayName,emailAddresses,jobTitle,companyName,department,officeLocation,businessPhones,mobilePhone')
620
+ .filter(`contains(displayName,'${query}') or contains(emailAddresses/any(e:e/address),'${query}')`)
621
+ .top(maxResults)
622
+ .get();
623
+ return result.value?.map((contact) => ({
624
+ id: contact.id,
625
+ displayName: contact.displayName || '',
626
+ emailAddresses: contact.emailAddresses?.map((email) => ({
627
+ name: email.name,
628
+ address: email.address
629
+ })) || [],
630
+ jobTitle: contact.jobTitle,
631
+ companyName: contact.companyName,
632
+ department: contact.department,
633
+ officeLocation: contact.officeLocation,
634
+ businessPhones: contact.businessPhones || [],
635
+ mobilePhone: contact.mobilePhone
636
+ })) || [];
637
+ }
638
+ catch (error) {
639
+ logger.error('Error searching contacts:', error);
640
+ throw error;
641
+ }
642
+ }
643
+ }
644
+ export const ms365Operations = new MS365Operations();