outlook-cli 1.2.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,282 @@
1
+ /**
2
+ * Improved search emails functionality
3
+ */
4
+ const config = require('../config');
5
+ const { callGraphAPI } = 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 count = Math.min(args.count || 10, config.MAX_RESULT_COUNT);
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
33
+ const response = await progressiveSearch(
34
+ endpoint,
35
+ accessToken,
36
+ { query, from, to, subject },
37
+ { hasAttachments, unreadOnly },
38
+ count
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} count - Maximum number of results
70
+ * @returns {Promise<object>} - Search results
71
+ */
72
+ async function progressiveSearch(endpoint, accessToken, searchTerms, filterTerms, count) {
73
+ // Track search strategies attempted
74
+ const searchAttempts = [];
75
+
76
+ // 1. Try combined search (most specific)
77
+ try {
78
+ const params = buildSearchParams(searchTerms, filterTerms, count);
79
+ console.error("Attempting combined search with params:", params);
80
+ searchAttempts.push("combined-search");
81
+
82
+ const response = await callGraphAPI(accessToken, 'GET', endpoint, null, params);
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
+ const simplifiedParams = {
102
+ $top: count,
103
+ $select: config.EMAIL_SELECT_FIELDS,
104
+ $orderby: 'receivedDateTime desc'
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 callGraphAPI(accessToken, 'GET', endpoint, null, simplifiedParams);
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: count,
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 callGraphAPI(accessToken, 'GET', endpoint, null, filterOnlyParams);
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
154
+ console.error("All search strategies failed, falling back to recent emails");
155
+ searchAttempts.push("recent-emails");
156
+
157
+ const basicParams = {
158
+ $top: count,
159
+ $select: config.EMAIL_SELECT_FIELDS,
160
+ $orderby: 'receivedDateTime desc'
161
+ };
162
+
163
+ const response = await callGraphAPI(accessToken, 'GET', endpoint, null, basicParams);
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
+ $orderby: 'receivedDateTime desc'
189
+ };
190
+
191
+ // Handle search terms
192
+ const kqlTerms = [];
193
+
194
+ if (searchTerms.query) {
195
+ // General query doesn't need a prefix
196
+ kqlTerms.push(searchTerms.query);
197
+ }
198
+
199
+ if (searchTerms.subject) {
200
+ kqlTerms.push(`subject:"${searchTerms.subject}"`);
201
+ }
202
+
203
+ if (searchTerms.from) {
204
+ kqlTerms.push(`from:"${searchTerms.from}"`);
205
+ }
206
+
207
+ if (searchTerms.to) {
208
+ kqlTerms.push(`to:"${searchTerms.to}"`);
209
+ }
210
+
211
+ // Add $search if we have any search terms
212
+ if (kqlTerms.length > 0) {
213
+ params.$search = kqlTerms.join(' ');
214
+ }
215
+
216
+ // Add boolean filters
217
+ addBooleanFilters(params, filterTerms);
218
+
219
+ return params;
220
+ }
221
+
222
+ /**
223
+ * Add boolean filters to query parameters
224
+ * @param {object} params - Query parameters
225
+ * @param {object} filterTerms - Filter terms (hasAttachments, unreadOnly)
226
+ */
227
+ function addBooleanFilters(params, filterTerms) {
228
+ const filterConditions = [];
229
+
230
+ if (filterTerms.hasAttachments === true) {
231
+ filterConditions.push('hasAttachments eq true');
232
+ }
233
+
234
+ if (filterTerms.unreadOnly === true) {
235
+ filterConditions.push('isRead eq false');
236
+ }
237
+
238
+ // Add $filter parameter if we have any filter conditions
239
+ if (filterConditions.length > 0) {
240
+ params.$filter = filterConditions.join(' and ');
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Format search results into a readable text format
246
+ * @param {object} response - The API response object
247
+ * @returns {object} - MCP response object
248
+ */
249
+ function formatSearchResults(response) {
250
+ if (!response.value || response.value.length === 0) {
251
+ return {
252
+ content: [{
253
+ type: "text",
254
+ text: `No emails found matching your search criteria.`
255
+ }]
256
+ };
257
+ }
258
+
259
+ // Format results
260
+ const emailList = response.value.map((email, index) => {
261
+ const sender = email.from?.emailAddress || { name: 'Unknown', address: 'unknown' };
262
+ const date = new Date(email.receivedDateTime).toLocaleString();
263
+ const readStatus = email.isRead ? '' : '[UNREAD] ';
264
+
265
+ return `${index + 1}. ${readStatus}${date} - From: ${sender.name} (${sender.address})\nSubject: ${email.subject}\nID: ${email.id}\n`;
266
+ }).join("\n");
267
+
268
+ // Add search strategy info if available
269
+ let additionalInfo = '';
270
+ if (response._searchInfo) {
271
+ additionalInfo = `\n(Search used ${response._searchInfo.strategies[response._searchInfo.strategies.length - 1]} strategy)`;
272
+ }
273
+
274
+ return {
275
+ content: [{
276
+ type: "text",
277
+ text: `Found ${response.value.length} emails matching your search criteria:${additionalInfo}\n\n${emailList}`
278
+ }]
279
+ };
280
+ }
281
+
282
+ 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;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Folder management module for Outlook MCP server
3
+ */
4
+ const handleListFolders = require('./list');
5
+ const handleCreateFolder = require('./create');
6
+ const handleMoveEmails = require('./move');
7
+
8
+ // Folder management tool definitions
9
+ const folderTools = [
10
+ {
11
+ name: "list-folders",
12
+ description: "Lists mail folders in your Outlook account",
13
+ inputSchema: {
14
+ type: "object",
15
+ properties: {
16
+ includeItemCounts: {
17
+ type: "boolean",
18
+ description: "Include counts of total and unread items"
19
+ },
20
+ includeChildren: {
21
+ type: "boolean",
22
+ description: "Include child folders in hierarchy"
23
+ }
24
+ },
25
+ required: []
26
+ },
27
+ handler: handleListFolders
28
+ },
29
+ {
30
+ name: "create-folder",
31
+ description: "Creates a new mail folder",
32
+ inputSchema: {
33
+ type: "object",
34
+ properties: {
35
+ name: {
36
+ type: "string",
37
+ description: "Name of the folder to create"
38
+ },
39
+ parentFolder: {
40
+ type: "string",
41
+ description: "Optional parent folder name (default is root)"
42
+ }
43
+ },
44
+ required: ["name"]
45
+ },
46
+ handler: handleCreateFolder
47
+ },
48
+ {
49
+ name: "move-emails",
50
+ description: "Moves emails from one folder to another",
51
+ inputSchema: {
52
+ type: "object",
53
+ properties: {
54
+ emailIds: {
55
+ type: "string",
56
+ description: "Comma-separated list of email IDs to move"
57
+ },
58
+ targetFolder: {
59
+ type: "string",
60
+ description: "Name of the folder to move emails to"
61
+ },
62
+ sourceFolder: {
63
+ type: "string",
64
+ description: "Optional name of the source folder (default is inbox)"
65
+ }
66
+ },
67
+ required: ["emailIds", "targetFolder"]
68
+ },
69
+ handler: handleMoveEmails
70
+ }
71
+ ];
72
+
73
+ module.exports = {
74
+ folderTools,
75
+ handleListFolders,
76
+ handleCreateFolder,
77
+ handleMoveEmails
78
+ };