@littlebearapps/outlook-assistant 3.3.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.
Files changed (52) hide show
  1. package/.env.example +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +422 -0
  4. package/advanced/index.js +652 -0
  5. package/auth/index.js +32 -0
  6. package/auth/oauth-server.js +233 -0
  7. package/auth/token-manager.js +105 -0
  8. package/auth/token-storage.js +359 -0
  9. package/auth/tools.js +159 -0
  10. package/calendar/accept.js +72 -0
  11. package/calendar/cancel.js +72 -0
  12. package/calendar/create.js +115 -0
  13. package/calendar/decline.js +72 -0
  14. package/calendar/delete.js +67 -0
  15. package/calendar/index.js +130 -0
  16. package/calendar/list.js +108 -0
  17. package/categories/index.js +955 -0
  18. package/config.js +95 -0
  19. package/contacts/index.js +754 -0
  20. package/email/attachments.js +365 -0
  21. package/email/conversations.js +666 -0
  22. package/email/delta.js +210 -0
  23. package/email/export.js +572 -0
  24. package/email/folder-utils.js +192 -0
  25. package/email/headers.js +344 -0
  26. package/email/index.js +537 -0
  27. package/email/list.js +136 -0
  28. package/email/mark-as-read.js +114 -0
  29. package/email/mime.js +286 -0
  30. package/email/read.js +161 -0
  31. package/email/search.js +628 -0
  32. package/email/send.js +169 -0
  33. package/folder/create.js +137 -0
  34. package/folder/delete.js +108 -0
  35. package/folder/index.js +112 -0
  36. package/folder/list.js +289 -0
  37. package/folder/move.js +186 -0
  38. package/folder/stats.js +322 -0
  39. package/index.js +162 -0
  40. package/llms.txt +76 -0
  41. package/outlook-auth-server.js +384 -0
  42. package/package.json +97 -0
  43. package/rules/create.js +273 -0
  44. package/rules/index.js +276 -0
  45. package/rules/list.js +216 -0
  46. package/settings/index.js +678 -0
  47. package/utils/field-presets.js +311 -0
  48. package/utils/graph-api.js +268 -0
  49. package/utils/mock-data.js +154 -0
  50. package/utils/odata-helpers.js +33 -0
  51. package/utils/response-formatter.js +457 -0
  52. package/utils/safety.js +123 -0
@@ -0,0 +1,273 @@
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
+ {
31
+ type: 'text',
32
+ text: 'Sequence must be a positive number greater than zero.',
33
+ },
34
+ ],
35
+ };
36
+ }
37
+
38
+ if (!name) {
39
+ return {
40
+ content: [
41
+ {
42
+ type: 'text',
43
+ text: 'Rule name is required.',
44
+ },
45
+ ],
46
+ };
47
+ }
48
+
49
+ // Validate that at least one condition or action is specified
50
+ const hasCondition =
51
+ fromAddresses || containsSubject || hasAttachments === true;
52
+ const hasAction = moveToFolder || markAsRead === true;
53
+
54
+ if (!hasCondition) {
55
+ return {
56
+ content: [
57
+ {
58
+ type: 'text',
59
+ text: 'At least one condition is required. Specify fromAddresses, containsSubject, or hasAttachments.',
60
+ },
61
+ ],
62
+ };
63
+ }
64
+
65
+ if (!hasAction) {
66
+ return {
67
+ content: [
68
+ {
69
+ type: 'text',
70
+ text: 'At least one action is required. Specify moveToFolder or markAsRead.',
71
+ },
72
+ ],
73
+ };
74
+ }
75
+
76
+ try {
77
+ // Get access token
78
+ const accessToken = await ensureAuthenticated();
79
+
80
+ // Create rule
81
+ const result = await createInboxRule(accessToken, {
82
+ name,
83
+ fromAddresses,
84
+ containsSubject,
85
+ hasAttachments,
86
+ moveToFolder,
87
+ markAsRead,
88
+ isEnabled,
89
+ sequence,
90
+ });
91
+
92
+ let responseText = result.message;
93
+
94
+ // Add a tip about sequence if it wasn't provided
95
+ if (!sequence && !result.error) {
96
+ responseText +=
97
+ "\n\nTip: You can specify a 'sequence' parameter when creating rules to control their execution order. Lower sequence numbers run first.";
98
+ }
99
+
100
+ return {
101
+ content: [
102
+ {
103
+ type: 'text',
104
+ text: responseText,
105
+ },
106
+ ],
107
+ };
108
+ } catch (error) {
109
+ if (error.message === 'Authentication required') {
110
+ return {
111
+ content: [
112
+ {
113
+ type: 'text',
114
+ text: "Authentication required. Please use the 'authenticate' tool first.",
115
+ },
116
+ ],
117
+ };
118
+ }
119
+
120
+ return {
121
+ content: [
122
+ {
123
+ type: 'text',
124
+ text: `Error creating rule: ${error.message}`,
125
+ },
126
+ ],
127
+ };
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Create a new inbox rule
133
+ * @param {string} accessToken - Access token
134
+ * @param {object} ruleOptions - Rule creation options
135
+ * @returns {Promise<object>} - Result object with status and message
136
+ */
137
+ async function createInboxRule(accessToken, ruleOptions) {
138
+ try {
139
+ const {
140
+ name,
141
+ fromAddresses,
142
+ containsSubject,
143
+ hasAttachments,
144
+ moveToFolder,
145
+ markAsRead,
146
+ isEnabled,
147
+ sequence,
148
+ } = ruleOptions;
149
+
150
+ // Get existing rules to determine sequence if not provided
151
+ let ruleSequence = sequence;
152
+ if (!ruleSequence) {
153
+ try {
154
+ // Default to 100 if we can't get existing rules
155
+ ruleSequence = 100;
156
+
157
+ // Get existing rules to find highest sequence
158
+ const existingRules = await getInboxRules(accessToken);
159
+ if (existingRules && existingRules.length > 0) {
160
+ // Find the highest sequence
161
+ const highestSequence = Math.max(
162
+ ...existingRules.map((r) => r.sequence || 0)
163
+ );
164
+ // Set new rule sequence to be higher
165
+ ruleSequence = Math.max(highestSequence + 1, 100);
166
+ console.error(
167
+ `Auto-generated sequence: ${ruleSequence} (based on highest existing: ${highestSequence})`
168
+ );
169
+ }
170
+ } catch (sequenceError) {
171
+ console.error(
172
+ `Error determining rule sequence: ${sequenceError.message}`
173
+ );
174
+ // Fall back to default value
175
+ ruleSequence = 100;
176
+ }
177
+ }
178
+
179
+ console.error(`Using rule sequence: ${ruleSequence}`);
180
+
181
+ // Make sure sequence is a positive integer
182
+ ruleSequence = Math.max(1, Math.floor(ruleSequence));
183
+
184
+ // Build rule object with sequence
185
+ const rule = {
186
+ displayName: name,
187
+ isEnabled: isEnabled === true,
188
+ sequence: ruleSequence,
189
+ conditions: {},
190
+ actions: {},
191
+ };
192
+
193
+ // Add conditions
194
+ if (fromAddresses) {
195
+ // Parse email addresses
196
+ const emailAddresses = fromAddresses
197
+ .split(',')
198
+ .map((email) => email.trim())
199
+ .filter((email) => email)
200
+ .map((email) => ({
201
+ emailAddress: {
202
+ address: email,
203
+ },
204
+ }));
205
+
206
+ if (emailAddresses.length > 0) {
207
+ rule.conditions.fromAddresses = emailAddresses;
208
+ }
209
+ }
210
+
211
+ if (containsSubject) {
212
+ rule.conditions.subjectContains = [containsSubject];
213
+ }
214
+
215
+ if (hasAttachments === true) {
216
+ rule.conditions.hasAttachment = true;
217
+ }
218
+
219
+ // Add actions
220
+ if (moveToFolder) {
221
+ // Get folder ID
222
+ try {
223
+ const folderId = await getFolderIdByName(accessToken, moveToFolder);
224
+ if (!folderId) {
225
+ return {
226
+ success: false,
227
+ message: `Target folder "${moveToFolder}" not found. Please specify a valid folder name.`,
228
+ };
229
+ }
230
+
231
+ rule.actions.moveToFolder = folderId;
232
+ } catch (folderError) {
233
+ console.error(
234
+ `Error resolving folder "${moveToFolder}": ${folderError.message}`
235
+ );
236
+ return {
237
+ success: false,
238
+ message: `Error resolving folder "${moveToFolder}": ${folderError.message}`,
239
+ };
240
+ }
241
+ }
242
+
243
+ if (markAsRead === true) {
244
+ rule.actions.markAsRead = true;
245
+ }
246
+
247
+ // Create the rule
248
+ const response = await callGraphAPI(
249
+ accessToken,
250
+ 'POST',
251
+ 'me/mailFolders/inbox/messageRules',
252
+ rule
253
+ );
254
+
255
+ if (response && response.id) {
256
+ return {
257
+ success: true,
258
+ message: `Successfully created rule "${name}" with sequence ${ruleSequence}.`,
259
+ ruleId: response.id,
260
+ };
261
+ } else {
262
+ return {
263
+ success: false,
264
+ message: "Failed to create rule. The server didn't return a rule ID.",
265
+ };
266
+ }
267
+ } catch (error) {
268
+ console.error(`Error creating rule: ${error.message}`);
269
+ throw error;
270
+ }
271
+ }
272
+
273
+ module.exports = handleCreateRule;
package/rules/index.js ADDED
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Email rules management module for Outlook Assistant server
3
+ */
4
+ const { handleListRules, getInboxRules } = require('./list');
5
+ const handleCreateRule = require('./create');
6
+ const { callGraphAPI } = require('../utils/graph-api');
7
+ const { ensureAuthenticated } = require('../auth');
8
+
9
+ /**
10
+ * Delete rule handler
11
+ * @param {object} args - Tool arguments
12
+ * @returns {object} - MCP response
13
+ */
14
+ async function handleDeleteRule(args) {
15
+ const { ruleName, ruleId } = args;
16
+
17
+ if (!ruleName && !ruleId) {
18
+ return {
19
+ content: [
20
+ {
21
+ type: 'text',
22
+ text: 'Either ruleName or ruleId is required.',
23
+ },
24
+ ],
25
+ };
26
+ }
27
+
28
+ try {
29
+ const accessToken = await ensureAuthenticated();
30
+
31
+ let resolvedId = ruleId;
32
+ let displayName = ruleId;
33
+
34
+ // Resolve name to ID if needed
35
+ if (!resolvedId && ruleName) {
36
+ const rules = await getInboxRules(accessToken);
37
+ const rule = rules.find((r) => r.displayName === ruleName);
38
+ if (!rule) {
39
+ return {
40
+ content: [
41
+ {
42
+ type: 'text',
43
+ text: `Rule with name "${ruleName}" not found.`,
44
+ },
45
+ ],
46
+ };
47
+ }
48
+ resolvedId = rule.id;
49
+ displayName = ruleName;
50
+ }
51
+
52
+ await callGraphAPI(
53
+ accessToken,
54
+ 'DELETE',
55
+ `me/mailFolders/inbox/messageRules/${resolvedId}`
56
+ );
57
+
58
+ return {
59
+ content: [
60
+ {
61
+ type: 'text',
62
+ text: `Successfully deleted rule "${displayName}".`,
63
+ },
64
+ ],
65
+ };
66
+ } catch (error) {
67
+ if (error.message === 'Authentication required') {
68
+ return {
69
+ content: [
70
+ {
71
+ type: 'text',
72
+ text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
73
+ },
74
+ ],
75
+ };
76
+ }
77
+ return {
78
+ content: [
79
+ {
80
+ type: 'text',
81
+ text: `Error deleting rule: ${error.message}`,
82
+ },
83
+ ],
84
+ };
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Edit rule sequence handler
90
+ * @param {object} args - Tool arguments
91
+ * @returns {object} - MCP response
92
+ */
93
+ async function handleEditRuleSequence(args) {
94
+ const { ruleName, sequence } = args;
95
+
96
+ if (!ruleName) {
97
+ return {
98
+ content: [
99
+ {
100
+ type: 'text',
101
+ text: 'Rule name is required. Please specify the exact name of an existing rule.',
102
+ },
103
+ ],
104
+ };
105
+ }
106
+
107
+ if (!sequence || isNaN(sequence) || sequence < 1) {
108
+ return {
109
+ content: [
110
+ {
111
+ type: 'text',
112
+ text: 'A positive sequence number is required. Lower numbers run first (higher priority).',
113
+ },
114
+ ],
115
+ };
116
+ }
117
+
118
+ try {
119
+ // Get access token
120
+ const accessToken = await ensureAuthenticated();
121
+
122
+ // Get all rules
123
+ const rules = await getInboxRules(accessToken);
124
+
125
+ // Find the rule by name
126
+ const rule = rules.find((r) => r.displayName === ruleName);
127
+ if (!rule) {
128
+ return {
129
+ content: [
130
+ {
131
+ type: 'text',
132
+ text: `Rule with name "${ruleName}" not found.`,
133
+ },
134
+ ],
135
+ };
136
+ }
137
+
138
+ // Update the rule sequence
139
+ const _updateResult = await callGraphAPI(
140
+ accessToken,
141
+ 'PATCH',
142
+ `me/mailFolders/inbox/messageRules/${rule.id}`,
143
+ {
144
+ sequence: sequence,
145
+ }
146
+ );
147
+
148
+ return {
149
+ content: [
150
+ {
151
+ type: 'text',
152
+ text: `Successfully updated the sequence of rule "${ruleName}" to ${sequence}.`,
153
+ },
154
+ ],
155
+ };
156
+ } catch (error) {
157
+ if (error.message === 'Authentication required') {
158
+ return {
159
+ content: [
160
+ {
161
+ type: 'text',
162
+ text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
163
+ },
164
+ ],
165
+ };
166
+ }
167
+
168
+ return {
169
+ content: [
170
+ {
171
+ type: 'text',
172
+ text: `Error updating rule sequence: ${error.message}`,
173
+ },
174
+ ],
175
+ };
176
+ }
177
+ }
178
+
179
+ // Consolidated rules tool definition
180
+ const rulesTools = [
181
+ {
182
+ name: 'manage-rules',
183
+ description:
184
+ 'Manage inbox rules. action=list (default) lists rules. action=create creates a new rule. action=reorder changes rule execution priority. action=delete removes a rule.',
185
+ annotations: {
186
+ title: 'Inbox Rules',
187
+ readOnlyHint: false,
188
+ destructiveHint: true,
189
+ openWorldHint: false,
190
+ },
191
+ inputSchema: {
192
+ type: 'object',
193
+ properties: {
194
+ action: {
195
+ type: 'string',
196
+ enum: ['list', 'create', 'reorder', 'delete'],
197
+ description: 'Action to perform (default: list)',
198
+ },
199
+ // list params
200
+ includeDetails: {
201
+ type: 'boolean',
202
+ description:
203
+ 'Include detailed rule conditions and actions (action=list)',
204
+ },
205
+ // create params
206
+ name: {
207
+ type: 'string',
208
+ description: 'Name of the rule to create (action=create, required)',
209
+ },
210
+ fromAddresses: {
211
+ type: 'string',
212
+ description: 'Comma-separated sender email addresses (action=create)',
213
+ },
214
+ containsSubject: {
215
+ type: 'string',
216
+ description: 'Subject text the email must contain (action=create)',
217
+ },
218
+ hasAttachments: {
219
+ type: 'boolean',
220
+ description: 'Apply to emails with attachments (action=create)',
221
+ },
222
+ moveToFolder: {
223
+ type: 'string',
224
+ description: 'Folder to move matching emails to (action=create)',
225
+ },
226
+ markAsRead: {
227
+ type: 'boolean',
228
+ description: 'Mark matching emails as read (action=create)',
229
+ },
230
+ isEnabled: {
231
+ type: 'boolean',
232
+ description:
233
+ 'Enable rule after creation, default: true (action=create)',
234
+ },
235
+ sequence: {
236
+ type: 'number',
237
+ description:
238
+ 'Execution order, lower numbers run first (action=create default: 100, action=reorder required)',
239
+ },
240
+ // reorder params
241
+ ruleName: {
242
+ type: 'string',
243
+ description:
244
+ 'Name of the rule (action=reorder required, action=delete alternative to ruleId)',
245
+ },
246
+ ruleId: {
247
+ type: 'string',
248
+ description: 'ID of the rule to delete (action=delete)',
249
+ },
250
+ },
251
+ required: [],
252
+ },
253
+ handler: async (args) => {
254
+ const action = args.action || 'list';
255
+ switch (action) {
256
+ case 'create':
257
+ return handleCreateRule(args);
258
+ case 'reorder':
259
+ return handleEditRuleSequence(args);
260
+ case 'delete':
261
+ return handleDeleteRule(args);
262
+ case 'list':
263
+ default:
264
+ return handleListRules(args);
265
+ }
266
+ },
267
+ },
268
+ ];
269
+
270
+ module.exports = {
271
+ rulesTools,
272
+ handleListRules,
273
+ handleCreateRule,
274
+ handleEditRuleSequence,
275
+ handleDeleteRule,
276
+ };