@tgai96/outlook-mcp 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/README.md +396 -0
- package/auth/index.js +64 -0
- package/auth/oauth-server.js +178 -0
- package/auth/token-manager.js +139 -0
- package/auth/token-storage.js +317 -0
- package/auth/tools.js +171 -0
- package/calendar/accept.js +64 -0
- package/calendar/cancel.js +64 -0
- package/calendar/create.js +69 -0
- package/calendar/decline.js +64 -0
- package/calendar/delete.js +59 -0
- package/calendar/index.js +123 -0
- package/calendar/list.js +77 -0
- package/cli.js +246 -0
- package/config.js +108 -0
- package/email/folder-utils.js +175 -0
- package/email/index.js +157 -0
- package/email/list.js +78 -0
- package/email/mark-as-read.js +101 -0
- package/email/read.js +128 -0
- package/email/search.js +285 -0
- package/email/send.js +120 -0
- package/folder/create.js +124 -0
- package/folder/index.js +78 -0
- package/folder/list.js +264 -0
- package/folder/move.js +163 -0
- package/index.js +148 -0
- package/package.json +54 -0
- package/rules/create.js +248 -0
- package/rules/index.js +175 -0
- package/rules/list.js +202 -0
- package/utils/graph-api.js +192 -0
- package/utils/mock-data.js +145 -0
- package/utils/odata-helpers.js +40 -0
package/email/read.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read email functionality
|
|
3
|
+
*/
|
|
4
|
+
const config = require('../config');
|
|
5
|
+
const { callGraphAPI } = require('../utils/graph-api');
|
|
6
|
+
const { ensureAuthenticated } = require('../auth');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Read email handler
|
|
10
|
+
* @param {object} args - Tool arguments
|
|
11
|
+
* @returns {object} - MCP response
|
|
12
|
+
*/
|
|
13
|
+
async function handleReadEmail(args) {
|
|
14
|
+
const emailId = args.id;
|
|
15
|
+
|
|
16
|
+
if (!emailId) {
|
|
17
|
+
return {
|
|
18
|
+
content: [{
|
|
19
|
+
type: "text",
|
|
20
|
+
text: "Email ID is required."
|
|
21
|
+
}]
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Get access token
|
|
27
|
+
const accessToken = await ensureAuthenticated();
|
|
28
|
+
|
|
29
|
+
// Make API call to get email details
|
|
30
|
+
const endpoint = `me/messages/${encodeURIComponent(emailId)}`;
|
|
31
|
+
const queryParams = {
|
|
32
|
+
$select: config.EMAIL_DETAIL_FIELDS
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const email = await callGraphAPI(accessToken, 'GET', endpoint, null, queryParams);
|
|
37
|
+
|
|
38
|
+
if (!email) {
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: "text",
|
|
43
|
+
text: `Email with ID ${emailId} not found.`
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Format sender, recipients, etc.
|
|
50
|
+
const sender = email.from ? `${email.from.emailAddress.name} (${email.from.emailAddress.address})` : 'Unknown';
|
|
51
|
+
const to = email.toRecipients ? email.toRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None';
|
|
52
|
+
const cc = email.ccRecipients && email.ccRecipients.length > 0 ? email.ccRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None';
|
|
53
|
+
const bcc = email.bccRecipients && email.bccRecipients.length > 0 ? email.bccRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None';
|
|
54
|
+
const date = new Date(email.receivedDateTime).toLocaleString();
|
|
55
|
+
|
|
56
|
+
// Extract body content
|
|
57
|
+
let body = '';
|
|
58
|
+
if (email.body) {
|
|
59
|
+
body = email.body.contentType === 'html' ?
|
|
60
|
+
// Simple HTML-to-text conversion for HTML bodies
|
|
61
|
+
email.body.content.replace(/<[^>]*>/g, '') :
|
|
62
|
+
email.body.content;
|
|
63
|
+
} else {
|
|
64
|
+
body = email.bodyPreview || 'No content';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Format the email
|
|
68
|
+
const formattedEmail = `From: ${sender}
|
|
69
|
+
To: ${to}
|
|
70
|
+
${cc !== 'None' ? `CC: ${cc}\n` : ''}${bcc !== 'None' ? `BCC: ${bcc}\n` : ''}Subject: ${email.subject}
|
|
71
|
+
Date: ${date}
|
|
72
|
+
Importance: ${email.importance || 'normal'}
|
|
73
|
+
Has Attachments: ${email.hasAttachments ? 'Yes' : 'No'}
|
|
74
|
+
|
|
75
|
+
${body}`;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
content: [
|
|
79
|
+
{
|
|
80
|
+
type: "text",
|
|
81
|
+
text: formattedEmail
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error(`Error reading email: ${error.message}`);
|
|
87
|
+
|
|
88
|
+
// Improved error handling with more specific messages
|
|
89
|
+
if (error.message.includes("doesn't belong to the targeted mailbox")) {
|
|
90
|
+
return {
|
|
91
|
+
content: [
|
|
92
|
+
{
|
|
93
|
+
type: "text",
|
|
94
|
+
text: `The email ID seems invalid or doesn't belong to your mailbox. Please try with a different email ID.`
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
};
|
|
98
|
+
} else {
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{
|
|
102
|
+
type: "text",
|
|
103
|
+
text: `Failed to read email: ${error.message}`
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error.message === 'Authentication required') {
|
|
111
|
+
return {
|
|
112
|
+
content: [{
|
|
113
|
+
type: "text",
|
|
114
|
+
text: "Authentication required. Please use the 'authenticate' tool first."
|
|
115
|
+
}]
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
content: [{
|
|
121
|
+
type: "text",
|
|
122
|
+
text: `Error accessing email: ${error.message}`
|
|
123
|
+
}]
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = handleReadEmail;
|
package/email/search.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Improved search emails functionality
|
|
3
|
+
*/
|
|
4
|
+
const config = require('../config');
|
|
5
|
+
const { callGraphAPI, callGraphAPIPaginated } = require('../utils/graph-api');
|
|
6
|
+
const { ensureAuthenticated } = require('../auth');
|
|
7
|
+
const { resolveFolderPath } = require('./folder-utils');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Search emails handler
|
|
11
|
+
* @param {object} args - Tool arguments
|
|
12
|
+
* @returns {object} - MCP response
|
|
13
|
+
*/
|
|
14
|
+
async function handleSearchEmails(args) {
|
|
15
|
+
const folder = args.folder || "inbox";
|
|
16
|
+
const requestedCount = args.count || 10;
|
|
17
|
+
const query = args.query || '';
|
|
18
|
+
const from = args.from || '';
|
|
19
|
+
const to = args.to || '';
|
|
20
|
+
const subject = args.subject || '';
|
|
21
|
+
const hasAttachments = args.hasAttachments;
|
|
22
|
+
const unreadOnly = args.unreadOnly;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Get access token
|
|
26
|
+
const accessToken = await ensureAuthenticated();
|
|
27
|
+
|
|
28
|
+
// Resolve the folder path
|
|
29
|
+
const endpoint = await resolveFolderPath(accessToken, folder);
|
|
30
|
+
console.error(`Using endpoint: ${endpoint} for folder: ${folder}`);
|
|
31
|
+
|
|
32
|
+
// Execute progressive search with pagination
|
|
33
|
+
const response = await progressiveSearch(
|
|
34
|
+
endpoint,
|
|
35
|
+
accessToken,
|
|
36
|
+
{ query, from, to, subject },
|
|
37
|
+
{ hasAttachments, unreadOnly },
|
|
38
|
+
requestedCount
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return formatSearchResults(response);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
// Handle authentication errors
|
|
44
|
+
if (error.message === 'Authentication required') {
|
|
45
|
+
return {
|
|
46
|
+
content: [{
|
|
47
|
+
type: "text",
|
|
48
|
+
text: "Authentication required. Please use the 'authenticate' tool first."
|
|
49
|
+
}]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// General error response
|
|
54
|
+
return {
|
|
55
|
+
content: [{
|
|
56
|
+
type: "text",
|
|
57
|
+
text: `Error searching emails: ${error.message}`
|
|
58
|
+
}]
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Execute a search with progressively simpler fallback strategies
|
|
65
|
+
* @param {string} endpoint - API endpoint
|
|
66
|
+
* @param {string} accessToken - Access token
|
|
67
|
+
* @param {object} searchTerms - Search terms (query, from, to, subject)
|
|
68
|
+
* @param {object} filterTerms - Filter terms (hasAttachments, unreadOnly)
|
|
69
|
+
* @param {number} maxCount - Maximum number of results to retrieve
|
|
70
|
+
* @returns {Promise<object>} - Search results
|
|
71
|
+
*/
|
|
72
|
+
async function progressiveSearch(endpoint, accessToken, searchTerms, filterTerms, maxCount) {
|
|
73
|
+
// Track search strategies attempted
|
|
74
|
+
const searchAttempts = [];
|
|
75
|
+
|
|
76
|
+
// 1. Try combined search (most specific)
|
|
77
|
+
try {
|
|
78
|
+
const params = buildSearchParams(searchTerms, filterTerms, Math.min(50, maxCount));
|
|
79
|
+
console.error("Attempting combined search with params:", params);
|
|
80
|
+
searchAttempts.push("combined-search");
|
|
81
|
+
|
|
82
|
+
const response = await callGraphAPIPaginated(accessToken, 'GET', endpoint, params, maxCount);
|
|
83
|
+
if (response.value && response.value.length > 0) {
|
|
84
|
+
console.error(`Combined search successful: found ${response.value.length} results`);
|
|
85
|
+
return response;
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error(`Combined search failed: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 2. Try each search term individually, starting with most specific
|
|
92
|
+
const searchPriority = ['subject', 'from', 'to', 'query'];
|
|
93
|
+
|
|
94
|
+
for (const term of searchPriority) {
|
|
95
|
+
if (searchTerms[term]) {
|
|
96
|
+
try {
|
|
97
|
+
console.error(`Attempting search with only ${term}: "${searchTerms[term]}"`);
|
|
98
|
+
searchAttempts.push(`single-term-${term}`);
|
|
99
|
+
|
|
100
|
+
// For single term search, only use $search with that term
|
|
101
|
+
// Note: $orderby cannot be used with $search in Microsoft Graph API
|
|
102
|
+
const simplifiedParams = {
|
|
103
|
+
$top: Math.min(50, maxCount),
|
|
104
|
+
$select: config.EMAIL_SELECT_FIELDS
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Add the search term in the appropriate KQL syntax
|
|
108
|
+
if (term === 'query') {
|
|
109
|
+
// General query doesn't need a prefix
|
|
110
|
+
simplifiedParams.$search = `"${searchTerms[term]}"`;
|
|
111
|
+
} else {
|
|
112
|
+
// Specific field searches use field:value syntax
|
|
113
|
+
simplifiedParams.$search = `${term}:"${searchTerms[term]}"`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Add boolean filters if applicable
|
|
117
|
+
addBooleanFilters(simplifiedParams, filterTerms);
|
|
118
|
+
|
|
119
|
+
const response = await callGraphAPIPaginated(accessToken, 'GET', endpoint, simplifiedParams, maxCount);
|
|
120
|
+
if (response.value && response.value.length > 0) {
|
|
121
|
+
console.error(`Search with ${term} successful: found ${response.value.length} results`);
|
|
122
|
+
return response;
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error(`Search with ${term} failed: ${error.message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 3. Try with only boolean filters
|
|
131
|
+
if (filterTerms.hasAttachments === true || filterTerms.unreadOnly === true) {
|
|
132
|
+
try {
|
|
133
|
+
console.error("Attempting search with only boolean filters");
|
|
134
|
+
searchAttempts.push("boolean-filters-only");
|
|
135
|
+
|
|
136
|
+
const filterOnlyParams = {
|
|
137
|
+
$top: Math.min(50, maxCount),
|
|
138
|
+
$select: config.EMAIL_SELECT_FIELDS,
|
|
139
|
+
$orderby: 'receivedDateTime desc'
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Add the boolean filters
|
|
143
|
+
addBooleanFilters(filterOnlyParams, filterTerms);
|
|
144
|
+
|
|
145
|
+
const response = await callGraphAPIPaginated(accessToken, 'GET', endpoint, filterOnlyParams, maxCount);
|
|
146
|
+
console.error(`Boolean filter search found ${response.value?.length || 0} results`);
|
|
147
|
+
return response;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error(`Boolean filter search failed: ${error.message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 4. Final fallback: just get recent emails with pagination
|
|
154
|
+
console.error("All search strategies failed, falling back to recent emails");
|
|
155
|
+
searchAttempts.push("recent-emails");
|
|
156
|
+
|
|
157
|
+
const basicParams = {
|
|
158
|
+
$top: Math.min(50, maxCount),
|
|
159
|
+
$select: config.EMAIL_SELECT_FIELDS,
|
|
160
|
+
$orderby: 'receivedDateTime desc'
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const response = await callGraphAPIPaginated(accessToken, 'GET', endpoint, basicParams, maxCount);
|
|
164
|
+
console.error(`Fallback to recent emails found ${response.value?.length || 0} results`);
|
|
165
|
+
|
|
166
|
+
// Add a note to the response about the search attempts
|
|
167
|
+
response._searchInfo = {
|
|
168
|
+
attemptsCount: searchAttempts.length,
|
|
169
|
+
strategies: searchAttempts,
|
|
170
|
+
originalTerms: searchTerms,
|
|
171
|
+
filterTerms: filterTerms
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return response;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Build search parameters from search terms and filter terms
|
|
179
|
+
* @param {object} searchTerms - Search terms (query, from, to, subject)
|
|
180
|
+
* @param {object} filterTerms - Filter terms (hasAttachments, unreadOnly)
|
|
181
|
+
* @param {number} count - Maximum number of results
|
|
182
|
+
* @returns {object} - Query parameters
|
|
183
|
+
*/
|
|
184
|
+
function buildSearchParams(searchTerms, filterTerms, count) {
|
|
185
|
+
const params = {
|
|
186
|
+
$top: count,
|
|
187
|
+
$select: config.EMAIL_SELECT_FIELDS
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Handle search terms
|
|
191
|
+
const kqlTerms = [];
|
|
192
|
+
|
|
193
|
+
if (searchTerms.query) {
|
|
194
|
+
// General query doesn't need a prefix
|
|
195
|
+
kqlTerms.push(searchTerms.query);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (searchTerms.subject) {
|
|
199
|
+
kqlTerms.push(`subject:"${searchTerms.subject}"`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (searchTerms.from) {
|
|
203
|
+
kqlTerms.push(`from:"${searchTerms.from}"`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (searchTerms.to) {
|
|
207
|
+
kqlTerms.push(`to:"${searchTerms.to}"`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Add $search if we have any search terms
|
|
211
|
+
// Note: $orderby cannot be used with $search in Microsoft Graph API
|
|
212
|
+
if (kqlTerms.length > 0) {
|
|
213
|
+
params.$search = kqlTerms.join(' ');
|
|
214
|
+
} else {
|
|
215
|
+
// Only add $orderby if we're not using $search
|
|
216
|
+
params.$orderby = 'receivedDateTime desc';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Add boolean filters
|
|
220
|
+
addBooleanFilters(params, filterTerms);
|
|
221
|
+
|
|
222
|
+
return params;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Add boolean filters to query parameters
|
|
227
|
+
* @param {object} params - Query parameters
|
|
228
|
+
* @param {object} filterTerms - Filter terms (hasAttachments, unreadOnly)
|
|
229
|
+
*/
|
|
230
|
+
function addBooleanFilters(params, filterTerms) {
|
|
231
|
+
const filterConditions = [];
|
|
232
|
+
|
|
233
|
+
if (filterTerms.hasAttachments === true) {
|
|
234
|
+
filterConditions.push('hasAttachments eq true');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (filterTerms.unreadOnly === true) {
|
|
238
|
+
filterConditions.push('isRead eq false');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Add $filter parameter if we have any filter conditions
|
|
242
|
+
if (filterConditions.length > 0) {
|
|
243
|
+
params.$filter = filterConditions.join(' and ');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Format search results into a readable text format
|
|
249
|
+
* @param {object} response - The API response object
|
|
250
|
+
* @returns {object} - MCP response object
|
|
251
|
+
*/
|
|
252
|
+
function formatSearchResults(response) {
|
|
253
|
+
if (!response.value || response.value.length === 0) {
|
|
254
|
+
return {
|
|
255
|
+
content: [{
|
|
256
|
+
type: "text",
|
|
257
|
+
text: `No emails found matching your search criteria.`
|
|
258
|
+
}]
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Format results
|
|
263
|
+
const emailList = response.value.map((email, index) => {
|
|
264
|
+
const sender = email.from?.emailAddress || { name: 'Unknown', address: 'unknown' };
|
|
265
|
+
const date = new Date(email.receivedDateTime).toLocaleString();
|
|
266
|
+
const readStatus = email.isRead ? '' : '[UNREAD] ';
|
|
267
|
+
|
|
268
|
+
return `${index + 1}. ${readStatus}${date} - From: ${sender.name} (${sender.address})\nSubject: ${email.subject}\nID: ${email.id}\n`;
|
|
269
|
+
}).join("\n");
|
|
270
|
+
|
|
271
|
+
// Add search strategy info if available
|
|
272
|
+
let additionalInfo = '';
|
|
273
|
+
if (response._searchInfo) {
|
|
274
|
+
additionalInfo = `\n(Search used ${response._searchInfo.strategies[response._searchInfo.strategies.length - 1]} strategy)`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
content: [{
|
|
279
|
+
type: "text",
|
|
280
|
+
text: `Found ${response.value.length} emails matching your search criteria:${additionalInfo}\n\n${emailList}`
|
|
281
|
+
}]
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = handleSearchEmails;
|
package/email/send.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send email functionality
|
|
3
|
+
*/
|
|
4
|
+
const config = require('../config');
|
|
5
|
+
const { callGraphAPI } = require('../utils/graph-api');
|
|
6
|
+
const { ensureAuthenticated } = require('../auth');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Send email handler
|
|
10
|
+
* @param {object} args - Tool arguments
|
|
11
|
+
* @returns {object} - MCP response
|
|
12
|
+
*/
|
|
13
|
+
async function handleSendEmail(args) {
|
|
14
|
+
const { to, cc, bcc, subject, body, importance = 'normal', saveToSentItems = true } = args;
|
|
15
|
+
|
|
16
|
+
// Validate required parameters
|
|
17
|
+
if (!to) {
|
|
18
|
+
return {
|
|
19
|
+
content: [{
|
|
20
|
+
type: "text",
|
|
21
|
+
text: "Recipient (to) is required."
|
|
22
|
+
}]
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!subject) {
|
|
27
|
+
return {
|
|
28
|
+
content: [{
|
|
29
|
+
type: "text",
|
|
30
|
+
text: "Subject is required."
|
|
31
|
+
}]
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!body) {
|
|
36
|
+
return {
|
|
37
|
+
content: [{
|
|
38
|
+
type: "text",
|
|
39
|
+
text: "Body content is required."
|
|
40
|
+
}]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Get access token
|
|
46
|
+
const accessToken = await ensureAuthenticated();
|
|
47
|
+
|
|
48
|
+
// Format recipients
|
|
49
|
+
const toRecipients = to.split(',').map(email => {
|
|
50
|
+
email = email.trim();
|
|
51
|
+
return {
|
|
52
|
+
emailAddress: {
|
|
53
|
+
address: email
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const ccRecipients = cc ? cc.split(',').map(email => {
|
|
59
|
+
email = email.trim();
|
|
60
|
+
return {
|
|
61
|
+
emailAddress: {
|
|
62
|
+
address: email
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}) : [];
|
|
66
|
+
|
|
67
|
+
const bccRecipients = bcc ? bcc.split(',').map(email => {
|
|
68
|
+
email = email.trim();
|
|
69
|
+
return {
|
|
70
|
+
emailAddress: {
|
|
71
|
+
address: email
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}) : [];
|
|
75
|
+
|
|
76
|
+
// Prepare email object
|
|
77
|
+
const emailObject = {
|
|
78
|
+
message: {
|
|
79
|
+
subject,
|
|
80
|
+
body: {
|
|
81
|
+
contentType: body.includes('<html') ? 'html' : 'text',
|
|
82
|
+
content: body
|
|
83
|
+
},
|
|
84
|
+
toRecipients,
|
|
85
|
+
ccRecipients: ccRecipients.length > 0 ? ccRecipients : undefined,
|
|
86
|
+
bccRecipients: bccRecipients.length > 0 ? bccRecipients : undefined,
|
|
87
|
+
importance
|
|
88
|
+
},
|
|
89
|
+
saveToSentItems
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Make API call to send email
|
|
93
|
+
await callGraphAPI(accessToken, 'POST', 'me/sendMail', emailObject);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
content: [{
|
|
97
|
+
type: "text",
|
|
98
|
+
text: `Email sent successfully!\n\nSubject: ${subject}\nRecipients: ${toRecipients.length}${ccRecipients.length > 0 ? ` + ${ccRecipients.length} CC` : ''}${bccRecipients.length > 0 ? ` + ${bccRecipients.length} BCC` : ''}\nMessage Length: ${body.length} characters`
|
|
99
|
+
}]
|
|
100
|
+
};
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error.message === 'Authentication required') {
|
|
103
|
+
return {
|
|
104
|
+
content: [{
|
|
105
|
+
type: "text",
|
|
106
|
+
text: "Authentication required. Please use the 'authenticate' tool first."
|
|
107
|
+
}]
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
content: [{
|
|
113
|
+
type: "text",
|
|
114
|
+
text: `Error sending email: ${error.message}`
|
|
115
|
+
}]
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = handleSendEmail;
|
package/folder/create.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create folder functionality
|
|
3
|
+
*/
|
|
4
|
+
const { callGraphAPI } = require('../utils/graph-api');
|
|
5
|
+
const { ensureAuthenticated } = require('../auth');
|
|
6
|
+
const { getFolderIdByName } = require('../email/folder-utils');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create folder handler
|
|
10
|
+
* @param {object} args - Tool arguments
|
|
11
|
+
* @returns {object} - MCP response
|
|
12
|
+
*/
|
|
13
|
+
async function handleCreateFolder(args) {
|
|
14
|
+
const folderName = args.name;
|
|
15
|
+
const parentFolder = args.parentFolder || '';
|
|
16
|
+
|
|
17
|
+
if (!folderName) {
|
|
18
|
+
return {
|
|
19
|
+
content: [{
|
|
20
|
+
type: "text",
|
|
21
|
+
text: "Folder name is required."
|
|
22
|
+
}]
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Get access token
|
|
28
|
+
const accessToken = await ensureAuthenticated();
|
|
29
|
+
|
|
30
|
+
// Create folder with appropriate parent
|
|
31
|
+
const result = await createMailFolder(accessToken, folderName, parentFolder);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
content: [{
|
|
35
|
+
type: "text",
|
|
36
|
+
text: result.message
|
|
37
|
+
}]
|
|
38
|
+
};
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error.message === 'Authentication required') {
|
|
41
|
+
return {
|
|
42
|
+
content: [{
|
|
43
|
+
type: "text",
|
|
44
|
+
text: "Authentication required. Please use the 'authenticate' tool first."
|
|
45
|
+
}]
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
content: [{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: `Error creating folder: ${error.message}`
|
|
53
|
+
}]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create a new mail folder
|
|
60
|
+
* @param {string} accessToken - Access token
|
|
61
|
+
* @param {string} folderName - Name of the folder to create
|
|
62
|
+
* @param {string} parentFolderName - Name of the parent folder (optional)
|
|
63
|
+
* @returns {Promise<object>} - Result object with status and message
|
|
64
|
+
*/
|
|
65
|
+
async function createMailFolder(accessToken, folderName, parentFolderName) {
|
|
66
|
+
try {
|
|
67
|
+
// Check if a folder with this name already exists
|
|
68
|
+
const existingFolder = await getFolderIdByName(accessToken, folderName);
|
|
69
|
+
if (existingFolder) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
message: `A folder named "${folderName}" already exists.`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// If parent folder specified, find its ID
|
|
77
|
+
let endpoint = 'me/mailFolders';
|
|
78
|
+
if (parentFolderName) {
|
|
79
|
+
const parentId = await getFolderIdByName(accessToken, parentFolderName);
|
|
80
|
+
if (!parentId) {
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
message: `Parent folder "${parentFolderName}" not found. Please specify a valid parent folder or leave it blank to create at the root level.`
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
endpoint = `me/mailFolders/${parentId}/childFolders`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Create the folder
|
|
91
|
+
const folderData = {
|
|
92
|
+
displayName: folderName
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const response = await callGraphAPI(
|
|
96
|
+
accessToken,
|
|
97
|
+
'POST',
|
|
98
|
+
endpoint,
|
|
99
|
+
folderData
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (response && response.id) {
|
|
103
|
+
const locationInfo = parentFolderName
|
|
104
|
+
? `inside "${parentFolderName}"`
|
|
105
|
+
: "at the root level";
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
success: true,
|
|
109
|
+
message: `Successfully created folder "${folderName}" ${locationInfo}.`,
|
|
110
|
+
folderId: response.id
|
|
111
|
+
};
|
|
112
|
+
} else {
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
message: "Failed to create folder. The server didn't return a folder ID."
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(`Error creating folder "${folderName}": ${error.message}`);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = handleCreateFolder;
|