@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/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;
@@ -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;
@@ -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;