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.
- package/LICENSE +21 -0
- package/README.md +497 -0
- package/bin/cli.js +247 -0
- package/dist/index.js +1251 -0
- package/dist/utils/api.js +43 -0
- package/dist/utils/credential-store.js +258 -0
- package/dist/utils/ms365-auth-enhanced.js +639 -0
- package/dist/utils/ms365-auth.js +363 -0
- package/dist/utils/ms365-operations.js +644 -0
- package/dist/utils/multi-user-auth.js +359 -0
- package/install.js +41 -0
- package/package.json +59 -0
|
@@ -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();
|