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.
package/rules/index.js ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Email rules management module for Outlook MCP server
3
+ */
4
+ const handleListRules = require('./list');
5
+ const handleCreateRule = require('./create');
6
+ const { ensureAuthenticated } = require('../auth');
7
+ const { callGraphAPI } = require('../utils/graph-api');
8
+
9
+ // Import getInboxRules for the edit sequence tool
10
+ const { getInboxRules } = require('./list');
11
+
12
+ /**
13
+ * Edit rule sequence handler
14
+ * @param {object} args - Tool arguments
15
+ * @returns {object} - MCP response
16
+ */
17
+ async function handleEditRuleSequence(args) {
18
+ const { ruleName, sequence } = args;
19
+
20
+ if (!ruleName) {
21
+ return {
22
+ content: [{
23
+ type: "text",
24
+ text: "Rule name is required. Please specify the exact name of an existing rule."
25
+ }]
26
+ };
27
+ }
28
+
29
+ if (!sequence || isNaN(sequence) || sequence < 1) {
30
+ return {
31
+ content: [{
32
+ type: "text",
33
+ text: "A positive sequence number is required. Lower numbers run first (higher priority)."
34
+ }]
35
+ };
36
+ }
37
+
38
+ try {
39
+ // Get access token
40
+ const accessToken = await ensureAuthenticated();
41
+
42
+ // Get all rules
43
+ const rules = await getInboxRules(accessToken);
44
+
45
+ // Find the rule by name
46
+ const rule = rules.find(r => r.displayName === ruleName);
47
+ if (!rule) {
48
+ return {
49
+ content: [{
50
+ type: "text",
51
+ text: `Rule with name "${ruleName}" not found.`
52
+ }]
53
+ };
54
+ }
55
+
56
+ // Update the rule sequence
57
+ const updateResult = await callGraphAPI(
58
+ accessToken,
59
+ 'PATCH',
60
+ `me/mailFolders/inbox/messageRules/${rule.id}`,
61
+ {
62
+ sequence: sequence
63
+ }
64
+ );
65
+
66
+ return {
67
+ content: [{
68
+ type: "text",
69
+ text: `Successfully updated the sequence of rule "${ruleName}" to ${sequence}.`
70
+ }]
71
+ };
72
+ } catch (error) {
73
+ if (error.message === 'Authentication required') {
74
+ return {
75
+ content: [{
76
+ type: "text",
77
+ text: "Authentication required. Please use the 'authenticate' tool first."
78
+ }]
79
+ };
80
+ }
81
+
82
+ return {
83
+ content: [{
84
+ type: "text",
85
+ text: `Error updating rule sequence: ${error.message}`
86
+ }]
87
+ };
88
+ }
89
+ }
90
+
91
+ // Rules management tool definitions
92
+ const rulesTools = [
93
+ {
94
+ name: "list-rules",
95
+ description: "Lists inbox rules in your Outlook account",
96
+ inputSchema: {
97
+ type: "object",
98
+ properties: {
99
+ includeDetails: {
100
+ type: "boolean",
101
+ description: "Include detailed rule conditions and actions"
102
+ }
103
+ },
104
+ required: []
105
+ },
106
+ handler: handleListRules
107
+ },
108
+ {
109
+ name: "create-rule",
110
+ description: "Creates a new inbox rule",
111
+ inputSchema: {
112
+ type: "object",
113
+ properties: {
114
+ name: {
115
+ type: "string",
116
+ description: "Name of the rule to create"
117
+ },
118
+ fromAddresses: {
119
+ type: "string",
120
+ description: "Comma-separated list of sender email addresses for the rule"
121
+ },
122
+ containsSubject: {
123
+ type: "string",
124
+ description: "Subject text the email must contain"
125
+ },
126
+ hasAttachments: {
127
+ type: "boolean",
128
+ description: "Whether the rule applies to emails with attachments"
129
+ },
130
+ moveToFolder: {
131
+ type: "string",
132
+ description: "Name of the folder to move matching emails to"
133
+ },
134
+ markAsRead: {
135
+ type: "boolean",
136
+ description: "Whether to mark matching emails as read"
137
+ },
138
+ isEnabled: {
139
+ type: "boolean",
140
+ description: "Whether the rule should be enabled after creation (default: true)"
141
+ },
142
+ sequence: {
143
+ type: "number",
144
+ description: "Order in which the rule is executed (lower numbers run first, default: 100)"
145
+ }
146
+ },
147
+ required: ["name"]
148
+ },
149
+ handler: handleCreateRule
150
+ },
151
+ {
152
+ name: "edit-rule-sequence",
153
+ description: "Changes the execution order of an existing inbox rule",
154
+ inputSchema: {
155
+ type: "object",
156
+ properties: {
157
+ ruleName: {
158
+ type: "string",
159
+ description: "Name of the rule to modify"
160
+ },
161
+ sequence: {
162
+ type: "number",
163
+ description: "New sequence value for the rule (lower numbers run first)"
164
+ }
165
+ },
166
+ required: ["ruleName", "sequence"]
167
+ },
168
+ handler: handleEditRuleSequence
169
+ }
170
+ ];
171
+
172
+ module.exports = {
173
+ rulesTools,
174
+ handleListRules,
175
+ handleCreateRule,
176
+ handleEditRuleSequence
177
+ };
package/rules/list.js ADDED
@@ -0,0 +1,202 @@
1
+ /**
2
+ * List rules functionality
3
+ */
4
+ const { callGraphAPI } = require('../utils/graph-api');
5
+ const { ensureAuthenticated } = require('../auth');
6
+
7
+ /**
8
+ * List rules handler
9
+ * @param {object} args - Tool arguments
10
+ * @returns {object} - MCP response
11
+ */
12
+ async function handleListRules(args) {
13
+ const includeDetails = args.includeDetails === true;
14
+
15
+ try {
16
+ // Get access token
17
+ const accessToken = await ensureAuthenticated();
18
+
19
+ // Get all inbox rules
20
+ const rules = await getInboxRules(accessToken);
21
+
22
+ // Format the rules based on detail level
23
+ const formattedRules = formatRulesList(rules, includeDetails);
24
+
25
+ return {
26
+ content: [{
27
+ type: "text",
28
+ text: formattedRules
29
+ }]
30
+ };
31
+ } catch (error) {
32
+ if (error.message === 'Authentication required') {
33
+ return {
34
+ content: [{
35
+ type: "text",
36
+ text: "Authentication required. Please use the 'authenticate' tool first."
37
+ }]
38
+ };
39
+ }
40
+
41
+ return {
42
+ content: [{
43
+ type: "text",
44
+ text: `Error listing rules: ${error.message}`
45
+ }]
46
+ };
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Get all inbox rules
52
+ * @param {string} accessToken - Access token
53
+ * @returns {Promise<Array>} - Array of rule objects
54
+ */
55
+ async function getInboxRules(accessToken) {
56
+ try {
57
+ const response = await callGraphAPI(
58
+ accessToken,
59
+ 'GET',
60
+ 'me/mailFolders/inbox/messageRules',
61
+ null
62
+ );
63
+
64
+ return response.value || [];
65
+ } catch (error) {
66
+ console.error(`Error getting inbox rules: ${error.message}`);
67
+ throw error;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Format rules list for display
73
+ * @param {Array} rules - Array of rule objects
74
+ * @param {boolean} includeDetails - Whether to include detailed conditions and actions
75
+ * @returns {string} - Formatted rules list
76
+ */
77
+ function formatRulesList(rules, includeDetails) {
78
+ if (!rules || rules.length === 0) {
79
+ return "No inbox rules found.\n\nTip: You can create rules using the 'create-rule' tool. Rules are processed in order of their sequence number (lower numbers are processed first).";
80
+ }
81
+
82
+ // Sort rules by sequence to show execution order
83
+ const sortedRules = [...rules].sort((a, b) => {
84
+ return (a.sequence || 9999) - (b.sequence || 9999);
85
+ });
86
+
87
+ // Format rules based on detail level
88
+ if (includeDetails) {
89
+ // Detailed format
90
+ const detailedRules = sortedRules.map((rule, index) => {
91
+ // Format rule header with sequence
92
+ let ruleText = `${index + 1}. ${rule.displayName}${rule.isEnabled ? '' : ' (Disabled)'} - Sequence: ${rule.sequence || 'N/A'}`;
93
+
94
+ // Format conditions
95
+ const conditions = formatRuleConditions(rule);
96
+ if (conditions) {
97
+ ruleText += `\n Conditions: ${conditions}`;
98
+ }
99
+
100
+ // Format actions
101
+ const actions = formatRuleActions(rule);
102
+ if (actions) {
103
+ ruleText += `\n Actions: ${actions}`;
104
+ }
105
+
106
+ return ruleText;
107
+ });
108
+
109
+ return `Found ${rules.length} inbox rules (sorted by execution order):\n\n${detailedRules.join('\n\n')}\n\nRules are processed in order of their sequence number. You can change rule order using the 'edit-rule-sequence' tool.`;
110
+ } else {
111
+ // Simple format
112
+ const simpleRules = sortedRules.map((rule, index) => {
113
+ return `${index + 1}. ${rule.displayName}${rule.isEnabled ? '' : ' (Disabled)'} - Sequence: ${rule.sequence || 'N/A'}`;
114
+ });
115
+
116
+ return `Found ${rules.length} inbox rules (sorted by execution order):\n\n${simpleRules.join('\n')}\n\nTip: Use 'list-rules with includeDetails=true' to see more information about each rule.`;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Format rule conditions for display
122
+ * @param {object} rule - Rule object
123
+ * @returns {string} - Formatted conditions
124
+ */
125
+ function formatRuleConditions(rule) {
126
+ const conditions = [];
127
+
128
+ // From addresses
129
+ if (rule.conditions?.fromAddresses?.length > 0) {
130
+ const senders = rule.conditions.fromAddresses.map(addr => addr.emailAddress.address).join(', ');
131
+ conditions.push(`From: ${senders}`);
132
+ }
133
+
134
+ // Subject contains
135
+ if (rule.conditions?.subjectContains?.length > 0) {
136
+ conditions.push(`Subject contains: "${rule.conditions.subjectContains.join(', ')}"`);
137
+ }
138
+
139
+ // Contains body text
140
+ if (rule.conditions?.bodyContains?.length > 0) {
141
+ conditions.push(`Body contains: "${rule.conditions.bodyContains.join(', ')}"`);
142
+ }
143
+
144
+ // Has attachment
145
+ if (rule.conditions?.hasAttachment === true) {
146
+ conditions.push('Has attachment');
147
+ }
148
+
149
+ // Importance
150
+ if (rule.conditions?.importance) {
151
+ conditions.push(`Importance: ${rule.conditions.importance}`);
152
+ }
153
+
154
+ return conditions.join('; ');
155
+ }
156
+
157
+ /**
158
+ * Format rule actions for display
159
+ * @param {object} rule - Rule object
160
+ * @returns {string} - Formatted actions
161
+ */
162
+ function formatRuleActions(rule) {
163
+ const actions = [];
164
+
165
+ // Move to folder
166
+ if (rule.actions?.moveToFolder) {
167
+ actions.push(`Move to folder: ${rule.actions.moveToFolder}`);
168
+ }
169
+
170
+ // Copy to folder
171
+ if (rule.actions?.copyToFolder) {
172
+ actions.push(`Copy to folder: ${rule.actions.copyToFolder}`);
173
+ }
174
+
175
+ // Mark as read
176
+ if (rule.actions?.markAsRead === true) {
177
+ actions.push('Mark as read');
178
+ }
179
+
180
+ // Mark importance
181
+ if (rule.actions?.markImportance) {
182
+ actions.push(`Mark importance: ${rule.actions.markImportance}`);
183
+ }
184
+
185
+ // Forward
186
+ if (rule.actions?.forwardTo?.length > 0) {
187
+ const recipients = rule.actions.forwardTo.map(r => r.emailAddress.address).join(', ');
188
+ actions.push(`Forward to: ${recipients}`);
189
+ }
190
+
191
+ // Delete
192
+ if (rule.actions?.delete === true) {
193
+ actions.push('Delete');
194
+ }
195
+
196
+ return actions.join('; ');
197
+ }
198
+
199
+ module.exports = {
200
+ handleListRules,
201
+ getInboxRules
202
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Shared tool registry used by both the MCP transport and local CLI.
3
+ */
4
+ const { authTools } = require('./auth');
5
+ const { calendarTools } = require('./calendar');
6
+ const { emailTools } = require('./email');
7
+ const { folderTools } = require('./folder');
8
+ const { rulesTools } = require('./rules');
9
+
10
+ const TOOLS = Object.freeze([
11
+ ...authTools,
12
+ ...calendarTools,
13
+ ...emailTools,
14
+ ...folderTools,
15
+ ...rulesTools
16
+ ]);
17
+
18
+ const TOOL_MAP = new Map(TOOLS.map((tool) => [tool.name, tool]));
19
+
20
+ function getTools() {
21
+ return TOOLS;
22
+ }
23
+
24
+ function listTools() {
25
+ return TOOLS.map((tool) => ({
26
+ name: tool.name,
27
+ description: tool.description,
28
+ inputSchema: tool.inputSchema
29
+ }));
30
+ }
31
+
32
+ function getTool(name) {
33
+ return TOOL_MAP.get(name);
34
+ }
35
+
36
+ async function invokeTool(name, args = {}) {
37
+ const tool = getTool(name);
38
+
39
+ if (!tool || typeof tool.handler !== 'function') {
40
+ const error = new Error(`Tool not found: ${name}`);
41
+ error.code = 'TOOL_NOT_FOUND';
42
+ throw error;
43
+ }
44
+
45
+ return tool.handler(args);
46
+ }
47
+
48
+ module.exports = {
49
+ TOOLS,
50
+ getTools,
51
+ listTools,
52
+ getTool,
53
+ invokeTool
54
+ };
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Microsoft Graph API helper functions
3
+ */
4
+ const https = require('https');
5
+ const config = require('../config');
6
+ const mockData = require('./mock-data');
7
+
8
+ /**
9
+ * Makes a request to the Microsoft Graph API
10
+ * @param {string} accessToken - The access token for authentication
11
+ * @param {string} method - HTTP method (GET, POST, etc.)
12
+ * @param {string} path - API endpoint path
13
+ * @param {object} data - Data to send for POST/PUT requests
14
+ * @param {object} queryParams - Query parameters
15
+ * @returns {Promise<object>} - The API response
16
+ */
17
+ async function callGraphAPI(accessToken, method, path, data = null, queryParams = {}) {
18
+ // For test tokens, we'll simulate the API call
19
+ if (config.USE_TEST_MODE && accessToken.startsWith('test_access_token_')) {
20
+ console.error(`TEST MODE: Simulating ${method} ${path} API call`);
21
+ return mockData.simulateGraphAPIResponse(method, path, data, queryParams);
22
+ }
23
+
24
+ try {
25
+ console.error(`Making real API call: ${method} ${path}`);
26
+
27
+ // Encode path segments properly
28
+ const encodedPath = path.split('/')
29
+ .map(segment => encodeURIComponent(segment))
30
+ .join('/');
31
+
32
+ // Build query string from parameters with special handling for OData filters
33
+ let queryString = '';
34
+ if (Object.keys(queryParams).length > 0) {
35
+ // Handle $filter parameter specially to ensure proper URI encoding
36
+ const filter = queryParams.$filter;
37
+ if (filter) {
38
+ delete queryParams.$filter; // Remove from regular params
39
+ }
40
+
41
+ // Build query string with proper encoding for regular params
42
+ const params = new URLSearchParams();
43
+ for (const [key, value] of Object.entries(queryParams)) {
44
+ params.append(key, value);
45
+ }
46
+
47
+ queryString = params.toString();
48
+
49
+ // Add filter parameter separately with proper encoding
50
+ if (filter) {
51
+ if (queryString) {
52
+ queryString += `&$filter=${encodeURIComponent(filter)}`;
53
+ } else {
54
+ queryString = `$filter=${encodeURIComponent(filter)}`;
55
+ }
56
+ }
57
+
58
+ if (queryString) {
59
+ queryString = '?' + queryString;
60
+ }
61
+
62
+ console.error(`Query string: ${queryString}`);
63
+ }
64
+
65
+ const url = `${config.GRAPH_API_ENDPOINT}${encodedPath}${queryString}`;
66
+ console.error(`Full URL: ${url}`);
67
+
68
+ return new Promise((resolve, reject) => {
69
+ const options = {
70
+ method: method,
71
+ headers: {
72
+ 'Authorization': `Bearer ${accessToken}`,
73
+ 'Content-Type': 'application/json'
74
+ }
75
+ };
76
+
77
+ const req = https.request(url, options, (res) => {
78
+ let responseData = '';
79
+
80
+ res.on('data', (chunk) => {
81
+ responseData += chunk;
82
+ });
83
+
84
+ res.on('end', () => {
85
+ if (res.statusCode >= 200 && res.statusCode < 300) {
86
+ try {
87
+ responseData = responseData ? responseData : '{}';
88
+ const jsonResponse = JSON.parse(responseData);
89
+ resolve(jsonResponse);
90
+ } catch (error) {
91
+ reject(new Error(`Error parsing API response: ${error.message}`));
92
+ }
93
+ } else if (res.statusCode === 401) {
94
+ // Token expired or invalid
95
+ reject(new Error('UNAUTHORIZED'));
96
+ } else {
97
+ reject(new Error(`API call failed with status ${res.statusCode}: ${responseData}`));
98
+ }
99
+ });
100
+ });
101
+
102
+ req.on('error', (error) => {
103
+ reject(new Error(`Network error during API call: ${error.message}`));
104
+ });
105
+
106
+ if (data && (method === 'POST' || method === 'PATCH' || method === 'PUT')) {
107
+ req.write(JSON.stringify(data));
108
+ }
109
+
110
+ req.end();
111
+ });
112
+ } catch (error) {
113
+ console.error('Error calling Graph API:', error);
114
+ throw error;
115
+ }
116
+ }
117
+
118
+ module.exports = {
119
+ callGraphAPI
120
+ };
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Mock data functions for test mode
3
+ */
4
+
5
+ /**
6
+ * Simulates Microsoft Graph API responses for testing
7
+ * @param {string} method - HTTP method
8
+ * @param {string} path - API path
9
+ * @param {object} data - Request data
10
+ * @param {object} queryParams - Query parameters
11
+ * @returns {object} - Simulated API response
12
+ */
13
+ function simulateGraphAPIResponse(method, path, data, queryParams) {
14
+ console.error(`Simulating response for: ${method} ${path}`);
15
+
16
+ if (method === 'GET') {
17
+ if (path.includes('messages') && !path.includes('sendMail')) {
18
+ // Simulate a successful email list/search response
19
+ if (path.includes('/messages/')) {
20
+ // Single email response
21
+ return {
22
+ id: "simulated-email-id",
23
+ subject: "Simulated Email Subject",
24
+ from: {
25
+ emailAddress: {
26
+ name: "Simulated Sender",
27
+ address: "sender@example.com"
28
+ }
29
+ },
30
+ toRecipients: [{
31
+ emailAddress: {
32
+ name: "Recipient Name",
33
+ address: "recipient@example.com"
34
+ }
35
+ }],
36
+ ccRecipients: [],
37
+ bccRecipients: [],
38
+ receivedDateTime: new Date().toISOString(),
39
+ bodyPreview: "This is a simulated email preview...",
40
+ body: {
41
+ contentType: "text",
42
+ content: "This is the full content of the simulated email. Since we can't connect to the real Microsoft Graph API, we're returning this placeholder content instead."
43
+ },
44
+ hasAttachments: false,
45
+ importance: "normal",
46
+ isRead: false,
47
+ internetMessageHeaders: []
48
+ };
49
+ } else {
50
+ // Email list response
51
+ return {
52
+ value: [
53
+ {
54
+ id: "simulated-email-1",
55
+ subject: "Important Meeting Tomorrow",
56
+ from: {
57
+ emailAddress: {
58
+ name: "John Doe",
59
+ address: "john@example.com"
60
+ }
61
+ },
62
+ toRecipients: [{
63
+ emailAddress: {
64
+ name: "You",
65
+ address: "you@example.com"
66
+ }
67
+ }],
68
+ ccRecipients: [],
69
+ receivedDateTime: new Date().toISOString(),
70
+ bodyPreview: "Let's discuss the project status...",
71
+ hasAttachments: false,
72
+ importance: "high",
73
+ isRead: false
74
+ },
75
+ {
76
+ id: "simulated-email-2",
77
+ subject: "Weekly Report",
78
+ from: {
79
+ emailAddress: {
80
+ name: "Jane Smith",
81
+ address: "jane@example.com"
82
+ }
83
+ },
84
+ toRecipients: [{
85
+ emailAddress: {
86
+ name: "You",
87
+ address: "you@example.com"
88
+ }
89
+ }],
90
+ ccRecipients: [],
91
+ receivedDateTime: new Date(Date.now() - 86400000).toISOString(), // Yesterday
92
+ bodyPreview: "Please find attached the weekly report...",
93
+ hasAttachments: true,
94
+ importance: "normal",
95
+ isRead: true
96
+ },
97
+ {
98
+ id: "simulated-email-3",
99
+ subject: "Question about the project",
100
+ from: {
101
+ emailAddress: {
102
+ name: "Bob Johnson",
103
+ address: "bob@example.com"
104
+ }
105
+ },
106
+ toRecipients: [{
107
+ emailAddress: {
108
+ name: "You",
109
+ address: "you@example.com"
110
+ }
111
+ }],
112
+ ccRecipients: [],
113
+ receivedDateTime: new Date(Date.now() - 172800000).toISOString(), // 2 days ago
114
+ bodyPreview: "I had a question about the timeline...",
115
+ hasAttachments: false,
116
+ importance: "normal",
117
+ isRead: false
118
+ }
119
+ ]
120
+ };
121
+ }
122
+ } else if (path.includes('mailFolders')) {
123
+ // Simulate a mail folders response
124
+ return {
125
+ value: [
126
+ { id: "inbox", displayName: "Inbox" },
127
+ { id: "drafts", displayName: "Drafts" },
128
+ { id: "sentItems", displayName: "Sent Items" },
129
+ { id: "deleteditems", displayName: "Deleted Items" }
130
+ ]
131
+ };
132
+ }
133
+ } else if (method === 'POST' && path.includes('sendMail')) {
134
+ // Simulate a successful email send
135
+ return {};
136
+ }
137
+
138
+ // If we get here, we don't have a simulation for this endpoint
139
+ console.error(`No simulation available for: ${method} ${path}`);
140
+ return {};
141
+ }
142
+
143
+ module.exports = {
144
+ simulateGraphAPIResponse
145
+ };