@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.
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Create rule functionality
3
+ */
4
+ const { callGraphAPI } = require('../utils/graph-api');
5
+ const { ensureAuthenticated } = require('../auth');
6
+ const { getFolderIdByName } = require('../email/folder-utils');
7
+ const { getInboxRules } = require('./list');
8
+
9
+ /**
10
+ * Create rule handler
11
+ * @param {object} args - Tool arguments
12
+ * @returns {object} - MCP response
13
+ */
14
+ async function handleCreateRule(args) {
15
+ const {
16
+ name,
17
+ fromAddresses,
18
+ containsSubject,
19
+ hasAttachments,
20
+ moveToFolder,
21
+ markAsRead,
22
+ isEnabled = true,
23
+ sequence
24
+ } = args;
25
+
26
+ // Add validation for sequence parameter
27
+ if (sequence !== undefined && (isNaN(sequence) || sequence < 1)) {
28
+ return {
29
+ content: [{
30
+ type: "text",
31
+ text: "Sequence must be a positive number greater than zero."
32
+ }]
33
+ };
34
+ }
35
+
36
+ if (!name) {
37
+ return {
38
+ content: [{
39
+ type: "text",
40
+ text: "Rule name is required."
41
+ }]
42
+ };
43
+ }
44
+
45
+ // Validate that at least one condition or action is specified
46
+ const hasCondition = fromAddresses || containsSubject || hasAttachments === true;
47
+ const hasAction = moveToFolder || markAsRead === true;
48
+
49
+ if (!hasCondition) {
50
+ return {
51
+ content: [{
52
+ type: "text",
53
+ text: "At least one condition is required. Specify fromAddresses, containsSubject, or hasAttachments."
54
+ }]
55
+ };
56
+ }
57
+
58
+ if (!hasAction) {
59
+ return {
60
+ content: [{
61
+ type: "text",
62
+ text: "At least one action is required. Specify moveToFolder or markAsRead."
63
+ }]
64
+ };
65
+ }
66
+
67
+ try {
68
+ // Get access token
69
+ const accessToken = await ensureAuthenticated();
70
+
71
+ // Create rule
72
+ const result = await createInboxRule(accessToken, {
73
+ name,
74
+ fromAddresses,
75
+ containsSubject,
76
+ hasAttachments,
77
+ moveToFolder,
78
+ markAsRead,
79
+ isEnabled,
80
+ sequence
81
+ });
82
+
83
+ let responseText = result.message;
84
+
85
+ // Add a tip about sequence if it wasn't provided
86
+ if (!sequence && !result.error) {
87
+ responseText += "\n\nTip: You can specify a 'sequence' parameter when creating rules to control their execution order. Lower sequence numbers run first.";
88
+ }
89
+
90
+ return {
91
+ content: [{
92
+ type: "text",
93
+ text: responseText
94
+ }]
95
+ };
96
+ } catch (error) {
97
+ if (error.message === 'Authentication required') {
98
+ return {
99
+ content: [{
100
+ type: "text",
101
+ text: "Authentication required. Please use the 'authenticate' tool first."
102
+ }]
103
+ };
104
+ }
105
+
106
+ return {
107
+ content: [{
108
+ type: "text",
109
+ text: `Error creating rule: ${error.message}`
110
+ }]
111
+ };
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Create a new inbox rule
117
+ * @param {string} accessToken - Access token
118
+ * @param {object} ruleOptions - Rule creation options
119
+ * @returns {Promise<object>} - Result object with status and message
120
+ */
121
+ async function createInboxRule(accessToken, ruleOptions) {
122
+ try {
123
+ const {
124
+ name,
125
+ fromAddresses,
126
+ containsSubject,
127
+ hasAttachments,
128
+ moveToFolder,
129
+ markAsRead,
130
+ isEnabled,
131
+ sequence
132
+ } = ruleOptions;
133
+
134
+ // Get existing rules to determine sequence if not provided
135
+ let ruleSequence = sequence;
136
+ if (!ruleSequence) {
137
+ try {
138
+ // Default to 100 if we can't get existing rules
139
+ ruleSequence = 100;
140
+
141
+ // Get existing rules to find highest sequence
142
+ const existingRules = await getInboxRules(accessToken);
143
+ if (existingRules && existingRules.length > 0) {
144
+ // Find the highest sequence
145
+ const highestSequence = Math.max(...existingRules.map(r => r.sequence || 0));
146
+ // Set new rule sequence to be higher
147
+ ruleSequence = Math.max(highestSequence + 1, 100);
148
+ console.error(`Auto-generated sequence: ${ruleSequence} (based on highest existing: ${highestSequence})`);
149
+ }
150
+ } catch (sequenceError) {
151
+ console.error(`Error determining rule sequence: ${sequenceError.message}`);
152
+ // Fall back to default value
153
+ ruleSequence = 100;
154
+ }
155
+ }
156
+
157
+ console.error(`Using rule sequence: ${ruleSequence}`);
158
+
159
+ // Make sure sequence is a positive integer
160
+ ruleSequence = Math.max(1, Math.floor(ruleSequence));
161
+
162
+ // Build rule object with sequence
163
+ const rule = {
164
+ displayName: name,
165
+ isEnabled: isEnabled === true,
166
+ sequence: ruleSequence,
167
+ conditions: {},
168
+ actions: {}
169
+ };
170
+
171
+ // Add conditions
172
+ if (fromAddresses) {
173
+ // Parse email addresses
174
+ const emailAddresses = fromAddresses.split(',')
175
+ .map(email => email.trim())
176
+ .filter(email => email)
177
+ .map(email => ({
178
+ emailAddress: {
179
+ address: email
180
+ }
181
+ }));
182
+
183
+ if (emailAddresses.length > 0) {
184
+ rule.conditions.fromAddresses = emailAddresses;
185
+ }
186
+ }
187
+
188
+ if (containsSubject) {
189
+ rule.conditions.subjectContains = [containsSubject];
190
+ }
191
+
192
+ if (hasAttachments === true) {
193
+ rule.conditions.hasAttachment = true;
194
+ }
195
+
196
+ // Add actions
197
+ if (moveToFolder) {
198
+ // Get folder ID
199
+ try {
200
+ const folderId = await getFolderIdByName(accessToken, moveToFolder);
201
+ if (!folderId) {
202
+ return {
203
+ success: false,
204
+ message: `Target folder "${moveToFolder}" not found. Please specify a valid folder name.`
205
+ };
206
+ }
207
+
208
+ rule.actions.moveToFolder = folderId;
209
+ } catch (folderError) {
210
+ console.error(`Error resolving folder "${moveToFolder}": ${folderError.message}`);
211
+ return {
212
+ success: false,
213
+ message: `Error resolving folder "${moveToFolder}": ${folderError.message}`
214
+ };
215
+ }
216
+ }
217
+
218
+ if (markAsRead === true) {
219
+ rule.actions.markAsRead = true;
220
+ }
221
+
222
+ // Create the rule
223
+ const response = await callGraphAPI(
224
+ accessToken,
225
+ 'POST',
226
+ 'me/mailFolders/inbox/messageRules',
227
+ rule
228
+ );
229
+
230
+ if (response && response.id) {
231
+ return {
232
+ success: true,
233
+ message: `Successfully created rule "${name}" with sequence ${ruleSequence}.`,
234
+ ruleId: response.id
235
+ };
236
+ } else {
237
+ return {
238
+ success: false,
239
+ message: "Failed to create rule. The server didn't return a rule ID."
240
+ };
241
+ }
242
+ } catch (error) {
243
+ console.error(`Error creating rule: ${error.message}`);
244
+ throw error;
245
+ }
246
+ }
247
+
248
+ module.exports = handleCreateRule;
package/rules/index.js ADDED
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Email rules management module for Outlook MCP server
3
+ */
4
+ const handleListRules = require('./list');
5
+ const handleCreateRule = require('./create');
6
+
7
+ // Import getInboxRules for the edit sequence tool
8
+ const { getInboxRules } = require('./list');
9
+
10
+ /**
11
+ * Edit rule sequence handler
12
+ * @param {object} args - Tool arguments
13
+ * @returns {object} - MCP response
14
+ */
15
+ async function handleEditRuleSequence(args) {
16
+ const { ruleName, sequence } = args;
17
+
18
+ if (!ruleName) {
19
+ return {
20
+ content: [{
21
+ type: "text",
22
+ text: "Rule name is required. Please specify the exact name of an existing rule."
23
+ }]
24
+ };
25
+ }
26
+
27
+ if (!sequence || isNaN(sequence) || sequence < 1) {
28
+ return {
29
+ content: [{
30
+ type: "text",
31
+ text: "A positive sequence number is required. Lower numbers run first (higher priority)."
32
+ }]
33
+ };
34
+ }
35
+
36
+ try {
37
+ // Get access token
38
+ const accessToken = await ensureAuthenticated();
39
+
40
+ // Get all rules
41
+ const rules = await getInboxRules(accessToken);
42
+
43
+ // Find the rule by name
44
+ const rule = rules.find(r => r.displayName === ruleName);
45
+ if (!rule) {
46
+ return {
47
+ content: [{
48
+ type: "text",
49
+ text: `Rule with name "${ruleName}" not found.`
50
+ }]
51
+ };
52
+ }
53
+
54
+ // Update the rule sequence
55
+ const updateResult = await callGraphAPI(
56
+ accessToken,
57
+ 'PATCH',
58
+ `me/mailFolders/inbox/messageRules/${rule.id}`,
59
+ {
60
+ sequence: sequence
61
+ }
62
+ );
63
+
64
+ return {
65
+ content: [{
66
+ type: "text",
67
+ text: `Successfully updated the sequence of rule "${ruleName}" to ${sequence}.`
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 updating rule sequence: ${error.message}`
84
+ }]
85
+ };
86
+ }
87
+ }
88
+
89
+ // Rules management tool definitions
90
+ const rulesTools = [
91
+ {
92
+ name: "list-rules",
93
+ description: "Lists inbox rules in your Outlook account",
94
+ inputSchema: {
95
+ type: "object",
96
+ properties: {
97
+ includeDetails: {
98
+ type: "boolean",
99
+ description: "Include detailed rule conditions and actions"
100
+ }
101
+ },
102
+ required: []
103
+ },
104
+ handler: handleListRules
105
+ },
106
+ {
107
+ name: "create-rule",
108
+ description: "Creates a new inbox rule",
109
+ inputSchema: {
110
+ type: "object",
111
+ properties: {
112
+ name: {
113
+ type: "string",
114
+ description: "Name of the rule to create"
115
+ },
116
+ fromAddresses: {
117
+ type: "string",
118
+ description: "Comma-separated list of sender email addresses for the rule"
119
+ },
120
+ containsSubject: {
121
+ type: "string",
122
+ description: "Subject text the email must contain"
123
+ },
124
+ hasAttachments: {
125
+ type: "boolean",
126
+ description: "Whether the rule applies to emails with attachments"
127
+ },
128
+ moveToFolder: {
129
+ type: "string",
130
+ description: "Name of the folder to move matching emails to"
131
+ },
132
+ markAsRead: {
133
+ type: "boolean",
134
+ description: "Whether to mark matching emails as read"
135
+ },
136
+ isEnabled: {
137
+ type: "boolean",
138
+ description: "Whether the rule should be enabled after creation (default: true)"
139
+ },
140
+ sequence: {
141
+ type: "number",
142
+ description: "Order in which the rule is executed (lower numbers run first, default: 100)"
143
+ }
144
+ },
145
+ required: ["name"]
146
+ },
147
+ handler: handleCreateRule
148
+ },
149
+ {
150
+ name: "edit-rule-sequence",
151
+ description: "Changes the execution order of an existing inbox rule",
152
+ inputSchema: {
153
+ type: "object",
154
+ properties: {
155
+ ruleName: {
156
+ type: "string",
157
+ description: "Name of the rule to modify"
158
+ },
159
+ sequence: {
160
+ type: "number",
161
+ description: "New sequence value for the rule (lower numbers run first)"
162
+ }
163
+ },
164
+ required: ["ruleName", "sequence"]
165
+ },
166
+ handler: handleEditRuleSequence
167
+ }
168
+ ];
169
+
170
+ module.exports = {
171
+ rulesTools,
172
+ handleListRules,
173
+ handleCreateRule,
174
+ handleEditRuleSequence
175
+ };
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
+ };