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,171 @@
1
+ /**
2
+ * Email folder utilities
3
+ */
4
+ const { callGraphAPI } = require('../utils/graph-api');
5
+
6
+ /**
7
+ * Cache of folder information to reduce API calls
8
+ * Format: { userId: { folderName: { id, path } } }
9
+ */
10
+ const folderCache = {};
11
+
12
+ /**
13
+ * Resolve a folder name to its endpoint path
14
+ * @param {string} accessToken - Access token
15
+ * @param {string} folderName - Folder name to resolve
16
+ * @returns {Promise<string>} - Resolved endpoint path
17
+ */
18
+ async function resolveFolderPath(accessToken, folderName) {
19
+ // Default to inbox if no folder specified
20
+ if (!folderName) {
21
+ return 'me/messages';
22
+ }
23
+
24
+ // Handle well-known folder names
25
+ const wellKnownFolders = {
26
+ 'inbox': 'me/messages',
27
+ 'drafts': 'me/mailFolders/drafts/messages',
28
+ 'sent': 'me/mailFolders/sentItems/messages',
29
+ 'deleted': 'me/mailFolders/deletedItems/messages',
30
+ 'junk': 'me/mailFolders/junkemail/messages',
31
+ 'archive': 'me/mailFolders/archive/messages'
32
+ };
33
+
34
+ // Check if it's a well-known folder (case-insensitive)
35
+ const lowerFolderName = folderName.toLowerCase();
36
+ if (wellKnownFolders[lowerFolderName]) {
37
+ console.error(`Using well-known folder path for "${folderName}"`);
38
+ return wellKnownFolders[lowerFolderName];
39
+ }
40
+
41
+ try {
42
+ // Try to find the folder by name
43
+ const folderId = await getFolderIdByName(accessToken, folderName);
44
+ if (folderId) {
45
+ const path = `me/mailFolders/${folderId}/messages`;
46
+ console.error(`Resolved folder "${folderName}" to path: ${path}`);
47
+ return path;
48
+ }
49
+
50
+ // If not found, fall back to inbox
51
+ console.error(`Couldn't find folder "${folderName}", falling back to inbox`);
52
+ return 'me/messages';
53
+ } catch (error) {
54
+ console.error(`Error resolving folder "${folderName}": ${error.message}`);
55
+ return 'me/messages';
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Get the ID of a mail folder by its name
61
+ * @param {string} accessToken - Access token
62
+ * @param {string} folderName - Name of the folder to find
63
+ * @returns {Promise<string|null>} - Folder ID or null if not found
64
+ */
65
+ async function getFolderIdByName(accessToken, folderName) {
66
+ try {
67
+ // First try with exact match filter
68
+ console.error(`Looking for folder with name "${folderName}"`);
69
+ const response = await callGraphAPI(
70
+ accessToken,
71
+ 'GET',
72
+ 'me/mailFolders',
73
+ null,
74
+ { $filter: `displayName eq '${folderName}'` }
75
+ );
76
+
77
+ if (response.value && response.value.length > 0) {
78
+ console.error(`Found folder "${folderName}" with ID: ${response.value[0].id}`);
79
+ return response.value[0].id;
80
+ }
81
+
82
+ // If exact match fails, try to get all folders and do a case-insensitive comparison
83
+ console.error(`No exact match found for "${folderName}", trying case-insensitive search`);
84
+ const allFoldersResponse = await callGraphAPI(
85
+ accessToken,
86
+ 'GET',
87
+ 'me/mailFolders',
88
+ null,
89
+ { $top: 100 }
90
+ );
91
+
92
+ if (allFoldersResponse.value) {
93
+ const lowerFolderName = folderName.toLowerCase();
94
+ const matchingFolder = allFoldersResponse.value.find(
95
+ folder => folder.displayName.toLowerCase() === lowerFolderName
96
+ );
97
+
98
+ if (matchingFolder) {
99
+ console.error(`Found case-insensitive match for "${folderName}" with ID: ${matchingFolder.id}`);
100
+ return matchingFolder.id;
101
+ }
102
+ }
103
+
104
+ console.error(`No folder found matching "${folderName}"`);
105
+ return null;
106
+ } catch (error) {
107
+ console.error(`Error finding folder "${folderName}": ${error.message}`);
108
+ return null;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Get all mail folders
114
+ * @param {string} accessToken - Access token
115
+ * @returns {Promise<Array>} - Array of folder objects
116
+ */
117
+ async function getAllFolders(accessToken) {
118
+ try {
119
+ // Get top-level folders
120
+ const response = await callGraphAPI(
121
+ accessToken,
122
+ 'GET',
123
+ 'me/mailFolders',
124
+ null,
125
+ {
126
+ $top: 100,
127
+ $select: 'id,displayName,parentFolderId,childFolderCount,totalItemCount,unreadItemCount'
128
+ }
129
+ );
130
+
131
+ if (!response.value) {
132
+ return [];
133
+ }
134
+
135
+ // Get child folders for folders with children
136
+ const foldersWithChildren = response.value.filter(f => f.childFolderCount > 0);
137
+
138
+ const childFolderPromises = foldersWithChildren.map(async (folder) => {
139
+ try {
140
+ const childResponse = await callGraphAPI(
141
+ accessToken,
142
+ 'GET',
143
+ `me/mailFolders/${folder.id}/childFolders`,
144
+ null,
145
+ {
146
+ $select: 'id,displayName,parentFolderId,childFolderCount,totalItemCount,unreadItemCount'
147
+ }
148
+ );
149
+
150
+ return childResponse.value || [];
151
+ } catch (error) {
152
+ console.error(`Error getting child folders for "${folder.displayName}": ${error.message}`);
153
+ return [];
154
+ }
155
+ });
156
+
157
+ const childFolders = await Promise.all(childFolderPromises);
158
+
159
+ // Combine top-level folders and all child folders
160
+ return [...response.value, ...childFolders.flat()];
161
+ } catch (error) {
162
+ console.error(`Error getting all folders: ${error.message}`);
163
+ return [];
164
+ }
165
+ }
166
+
167
+ module.exports = {
168
+ resolveFolderPath,
169
+ getFolderIdByName,
170
+ getAllFolders
171
+ };
package/email/index.js ADDED
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Email module for Outlook MCP server
3
+ */
4
+ const handleListEmails = require('./list');
5
+ const handleSearchEmails = require('./search');
6
+ const handleReadEmail = require('./read');
7
+ const handleSendEmail = require('./send');
8
+ const handleMarkAsRead = require('./mark-as-read');
9
+
10
+ // Email tool definitions
11
+ const emailTools = [
12
+ {
13
+ name: "list-emails",
14
+ description: "Lists recent emails from your inbox",
15
+ inputSchema: {
16
+ type: "object",
17
+ properties: {
18
+ folder: {
19
+ type: "string",
20
+ description: "Email folder to list (e.g., 'inbox', 'sent', 'drafts', default: 'inbox')"
21
+ },
22
+ count: {
23
+ type: "number",
24
+ description: "Number of emails to retrieve (default: 10, max: 50)"
25
+ }
26
+ },
27
+ required: []
28
+ },
29
+ handler: handleListEmails
30
+ },
31
+ {
32
+ name: "search-emails",
33
+ description: "Search for emails using various criteria",
34
+ inputSchema: {
35
+ type: "object",
36
+ properties: {
37
+ query: {
38
+ type: "string",
39
+ description: "Search query text to find in emails"
40
+ },
41
+ folder: {
42
+ type: "string",
43
+ description: "Email folder to search in (default: 'inbox')"
44
+ },
45
+ from: {
46
+ type: "string",
47
+ description: "Filter by sender email address or name"
48
+ },
49
+ to: {
50
+ type: "string",
51
+ description: "Filter by recipient email address or name"
52
+ },
53
+ subject: {
54
+ type: "string",
55
+ description: "Filter by email subject"
56
+ },
57
+ hasAttachments: {
58
+ type: "boolean",
59
+ description: "Filter to only emails with attachments"
60
+ },
61
+ unreadOnly: {
62
+ type: "boolean",
63
+ description: "Filter to only unread emails"
64
+ },
65
+ count: {
66
+ type: "number",
67
+ description: "Number of results to return (default: 10, max: 50)"
68
+ }
69
+ },
70
+ required: []
71
+ },
72
+ handler: handleSearchEmails
73
+ },
74
+ {
75
+ name: "read-email",
76
+ description: "Reads the content of a specific email",
77
+ inputSchema: {
78
+ type: "object",
79
+ properties: {
80
+ id: {
81
+ type: "string",
82
+ description: "ID of the email to read"
83
+ }
84
+ },
85
+ required: ["id"]
86
+ },
87
+ handler: handleReadEmail
88
+ },
89
+ {
90
+ name: "send-email",
91
+ description: "Composes and sends a new email",
92
+ inputSchema: {
93
+ type: "object",
94
+ properties: {
95
+ to: {
96
+ type: "string",
97
+ description: "Comma-separated list of recipient email addresses"
98
+ },
99
+ cc: {
100
+ type: "string",
101
+ description: "Comma-separated list of CC recipient email addresses"
102
+ },
103
+ bcc: {
104
+ type: "string",
105
+ description: "Comma-separated list of BCC recipient email addresses"
106
+ },
107
+ subject: {
108
+ type: "string",
109
+ description: "Email subject"
110
+ },
111
+ body: {
112
+ type: "string",
113
+ description: "Email body content (can be plain text or HTML)"
114
+ },
115
+ importance: {
116
+ type: "string",
117
+ description: "Email importance (normal, high, low)",
118
+ enum: ["normal", "high", "low"]
119
+ },
120
+ saveToSentItems: {
121
+ type: "boolean",
122
+ description: "Whether to save the email to sent items"
123
+ }
124
+ },
125
+ required: ["to", "subject", "body"]
126
+ },
127
+ handler: handleSendEmail
128
+ },
129
+ {
130
+ name: "mark-as-read",
131
+ description: "Marks an email as read or unread",
132
+ inputSchema: {
133
+ type: "object",
134
+ properties: {
135
+ id: {
136
+ type: "string",
137
+ description: "ID of the email to mark as read/unread"
138
+ },
139
+ isRead: {
140
+ type: "boolean",
141
+ description: "Whether to mark as read (true) or unread (false). Default: true"
142
+ }
143
+ },
144
+ required: ["id"]
145
+ },
146
+ handler: handleMarkAsRead
147
+ }
148
+ ];
149
+
150
+ module.exports = {
151
+ emailTools,
152
+ handleListEmails,
153
+ handleSearchEmails,
154
+ handleReadEmail,
155
+ handleSendEmail,
156
+ handleMarkAsRead
157
+ };
package/email/list.js ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * List emails functionality
3
+ */
4
+ const config = require('../config');
5
+ const { callGraphAPI } = require('../utils/graph-api');
6
+ const { ensureAuthenticated } = require('../auth');
7
+
8
+ /**
9
+ * List emails handler
10
+ * @param {object} args - Tool arguments
11
+ * @returns {object} - MCP response
12
+ */
13
+ async function handleListEmails(args) {
14
+ const folder = args.folder || "inbox";
15
+ const count = Math.min(args.count || 10, config.MAX_RESULT_COUNT);
16
+
17
+ try {
18
+ // Get access token
19
+ const accessToken = await ensureAuthenticated();
20
+
21
+ // Build API endpoint
22
+ let endpoint = 'me/messages';
23
+ if (folder.toLowerCase() !== 'inbox') {
24
+ // Get folder ID first if not inbox
25
+ const folderResponse = await callGraphAPI(
26
+ accessToken,
27
+ 'GET',
28
+ `me/mailFolders?$filter=displayName eq '${folder}'`
29
+ );
30
+
31
+ if (folderResponse.value && folderResponse.value.length > 0) {
32
+ endpoint = `me/mailFolders/${folderResponse.value[0].id}/messages`;
33
+ }
34
+ }
35
+
36
+ // Add query parameters
37
+ const queryParams = {
38
+ $top: count,
39
+ $orderby: 'receivedDateTime desc',
40
+ $select: config.EMAIL_SELECT_FIELDS
41
+ };
42
+
43
+ // Make API call
44
+ const response = await callGraphAPI(accessToken, 'GET', endpoint, null, queryParams);
45
+
46
+ if (!response.value || response.value.length === 0) {
47
+ return {
48
+ content: [{
49
+ type: "text",
50
+ text: `No emails found in ${folder}.`
51
+ }]
52
+ };
53
+ }
54
+
55
+ // Format results
56
+ const emailList = response.value.map((email, index) => {
57
+ const sender = email.from ? email.from.emailAddress : { name: 'Unknown', address: 'unknown' };
58
+ const date = new Date(email.receivedDateTime).toLocaleString();
59
+ const readStatus = email.isRead ? '' : '[UNREAD] ';
60
+
61
+ return `${index + 1}. ${readStatus}${date} - From: ${sender.name} (${sender.address})\nSubject: ${email.subject}\nID: ${email.id}\n`;
62
+ }).join("\n");
63
+
64
+ return {
65
+ content: [{
66
+ type: "text",
67
+ text: `Found ${response.value.length} emails in ${folder}:\n\n${emailList}`
68
+ }]
69
+ };
70
+ } catch (error) {
71
+ if (error.message === 'Authentication required') {
72
+ return {
73
+ content: [{
74
+ type: "text",
75
+ text: "Authentication required. Please use the 'authenticate' tool first."
76
+ }]
77
+ };
78
+ }
79
+
80
+ return {
81
+ content: [{
82
+ type: "text",
83
+ text: `Error listing emails: ${error.message}`
84
+ }]
85
+ };
86
+ }
87
+ }
88
+
89
+ module.exports = handleListEmails;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Mark email as read functionality
3
+ */
4
+ const config = require('../config');
5
+ const { callGraphAPI } = require('../utils/graph-api');
6
+ const { ensureAuthenticated } = require('../auth');
7
+
8
+ /**
9
+ * Mark email as read handler
10
+ * @param {object} args - Tool arguments
11
+ * @returns {object} - MCP response
12
+ */
13
+ async function handleMarkAsRead(args) {
14
+ const emailId = args.id;
15
+ const isRead = args.isRead !== undefined ? args.isRead : true; // Default to true
16
+
17
+ if (!emailId) {
18
+ return {
19
+ content: [{
20
+ type: "text",
21
+ text: "Email ID is required."
22
+ }]
23
+ };
24
+ }
25
+
26
+ try {
27
+ // Get access token
28
+ const accessToken = await ensureAuthenticated();
29
+
30
+ // Make API call to update email read status
31
+ const endpoint = `me/messages/${encodeURIComponent(emailId)}`;
32
+ const updateData = {
33
+ isRead: isRead
34
+ };
35
+
36
+ try {
37
+ const result = await callGraphAPI(accessToken, 'PATCH', endpoint, updateData);
38
+
39
+ const status = isRead ? 'read' : 'unread';
40
+
41
+ return {
42
+ content: [
43
+ {
44
+ type: "text",
45
+ text: `Email successfully marked as ${status}.`
46
+ }
47
+ ]
48
+ };
49
+ } catch (error) {
50
+ console.error(`Error marking email as ${isRead ? 'read' : 'unread'}: ${error.message}`);
51
+
52
+ // Improved error handling with more specific messages
53
+ if (error.message.includes("doesn't belong to the targeted mailbox")) {
54
+ return {
55
+ content: [
56
+ {
57
+ type: "text",
58
+ text: `The email ID seems invalid or doesn't belong to your mailbox. Please try with a different email ID.`
59
+ }
60
+ ]
61
+ };
62
+ } else if (error.message.includes("UNAUTHORIZED")) {
63
+ return {
64
+ content: [
65
+ {
66
+ type: "text",
67
+ text: "Authentication failed. Please re-authenticate and try again."
68
+ }
69
+ ]
70
+ };
71
+ } else {
72
+ return {
73
+ content: [
74
+ {
75
+ type: "text",
76
+ text: `Failed to mark email as ${isRead ? 'read' : 'unread'}: ${error.message}`
77
+ }
78
+ ]
79
+ };
80
+ }
81
+ }
82
+ } catch (error) {
83
+ if (error.message === 'Authentication required') {
84
+ return {
85
+ content: [{
86
+ type: "text",
87
+ text: "Authentication required. Please use the 'authenticate' tool first."
88
+ }]
89
+ };
90
+ }
91
+
92
+ return {
93
+ content: [{
94
+ type: "text",
95
+ text: `Error accessing email: ${error.message}`
96
+ }]
97
+ };
98
+ }
99
+ }
100
+
101
+ module.exports = handleMarkAsRead;
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;