@littlebearapps/outlook-assistant 3.6.0 → 3.7.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/create.js CHANGED
@@ -3,8 +3,16 @@
3
3
  */
4
4
  const { callGraphAPI } = require('../utils/graph-api');
5
5
  const { ensureAuthenticated } = require('../auth');
6
- const { getFolderIdByName } = require('../email/folder-utils');
6
+ const { checkRateLimit } = require('../utils/safety');
7
+ const { formatRuleDryRunPreview } = require('../utils/safety');
7
8
  const { getInboxRules } = require('./list');
9
+ const {
10
+ buildConditions,
11
+ buildActions,
12
+ buildExceptions,
13
+ hasAnyCondition,
14
+ hasAnyAction,
15
+ } = require('./rule-builder');
8
16
 
9
17
  /**
10
18
  * Create rule handler
@@ -12,18 +20,13 @@ const { getInboxRules } = require('./list');
12
20
  * @returns {object} - MCP response
13
21
  */
14
22
  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;
23
+ const { name, isEnabled = true, sequence, dryRun } = args;
25
24
 
26
- // Add validation for sequence parameter
25
+ // Rate limit rule creation
26
+ const rateLimitError = checkRateLimit('manage-rules');
27
+ if (rateLimitError) return rateLimitError;
28
+
29
+ // Validate sequence parameter
27
30
  if (sequence !== undefined && (isNaN(sequence) || sequence < 1)) {
28
31
  return {
29
32
  content: [
@@ -46,202 +49,96 @@ async function handleCreateRule(args) {
46
49
  };
47
50
  }
48
51
 
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) {
52
+ if (!hasAnyCondition(args)) {
55
53
  return {
56
54
  content: [
57
55
  {
58
56
  type: 'text',
59
- text: 'At least one condition is required. Specify fromAddresses, containsSubject, or hasAttachments.',
57
+ text: 'At least one condition is required. Available conditions: fromAddresses, containsSubject, bodyContains, bodyOrSubjectContains, senderContains, recipientContains, sentToAddresses, hasAttachments, importance, sensitivity, sentToMe, sentOnlyToMe, sentCcMe, isAutomaticReply.',
60
58
  },
61
59
  ],
62
60
  };
63
61
  }
64
62
 
65
- if (!hasAction) {
63
+ if (!hasAnyAction(args)) {
66
64
  return {
67
65
  content: [
68
66
  {
69
67
  type: 'text',
70
- text: 'At least one action is required. Specify moveToFolder or markAsRead.',
68
+ text: 'At least one action is required. Available actions: moveToFolder, copyToFolder, markAsRead, markImportance, forwardTo, redirectTo, assignCategories, stopProcessingRules, deleteMessage.',
71
69
  },
72
70
  ],
73
71
  };
74
72
  }
75
73
 
76
74
  try {
77
- // Get access token
78
75
  const accessToken = await ensureAuthenticated();
79
76
 
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;
77
+ // Build rule components
78
+ const { conditions, warnings: condWarnings } = buildConditions(args);
79
+ const { actions, warnings: actWarnings } = await buildActions(
80
+ args,
81
+ accessToken
82
+ );
83
+ const { exceptions, warnings: excWarnings } = buildExceptions(args);
93
84
 
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
- }
85
+ const allWarnings = [...condWarnings, ...actWarnings, ...excWarnings];
99
86
 
100
- return {
101
- content: [
102
- {
103
- type: 'text',
104
- text: responseText,
105
- },
106
- ],
107
- };
108
- } catch (error) {
109
- if (error.message === 'Authentication required') {
87
+ // Check for fatal warnings (folder not found = no valid action)
88
+ const folderNotFound = actWarnings.some((w) => w.includes('not found'));
89
+ if (folderNotFound && Object.keys(actions).length === 0) {
110
90
  return {
111
91
  content: [
112
92
  {
113
93
  type: 'text',
114
- text: "Authentication required. Please use the 'authenticate' tool first.",
94
+ text: actWarnings.filter((w) => w.includes('not found')).join('\n'),
115
95
  },
116
96
  ],
117
97
  };
118
98
  }
119
99
 
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
100
+ // Determine sequence
151
101
  let ruleSequence = sequence;
152
102
  if (!ruleSequence) {
103
+ ruleSequence = 100;
153
104
  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
105
  const existingRules = await getInboxRules(accessToken);
159
106
  if (existingRules && existingRules.length > 0) {
160
- // Find the highest sequence
161
107
  const highestSequence = Math.max(
162
108
  ...existingRules.map((r) => r.sequence || 0)
163
109
  );
164
- // Set new rule sequence to be higher
165
110
  ruleSequence = Math.max(highestSequence + 1, 100);
166
- console.error(
167
- `Auto-generated sequence: ${ruleSequence} (based on highest existing: ${highestSequence})`
168
- );
169
111
  }
170
- } catch (sequenceError) {
171
- console.error(
172
- `Error determining rule sequence: ${sequenceError.message}`
173
- );
174
- // Fall back to default value
112
+ } catch (_sequenceError) {
175
113
  ruleSequence = 100;
176
114
  }
177
115
  }
178
-
179
- console.error(`Using rule sequence: ${ruleSequence}`);
180
-
181
- // Make sure sequence is a positive integer
182
116
  ruleSequence = Math.max(1, Math.floor(ruleSequence));
183
117
 
184
- // Build rule object with sequence
118
+ // Build the rule object
185
119
  const rule = {
186
120
  displayName: name,
187
121
  isEnabled: isEnabled === true,
188
122
  sequence: ruleSequence,
189
- conditions: {},
190
- actions: {},
123
+ conditions,
124
+ actions,
191
125
  };
192
126
 
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;
127
+ // Add exceptions if any were specified
128
+ if (Object.keys(exceptions).length > 0) {
129
+ rule.exceptions = exceptions;
217
130
  }
218
131
 
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
- };
132
+ // Dry-run: preview without creating
133
+ if (dryRun) {
134
+ const preview = formatRuleDryRunPreview(rule);
135
+ let text = `DRY RUN — Rule preview (not created):\n\n${preview}`;
136
+ if (allWarnings.length > 0) {
137
+ text += `\n\nWarnings:\n${allWarnings.map((w) => `- ${w}`).join('\n')}`;
240
138
  }
241
- }
242
-
243
- if (markAsRead === true) {
244
- rule.actions.markAsRead = true;
139
+ return {
140
+ content: [{ type: 'text', text }],
141
+ };
245
142
  }
246
143
 
247
144
  // Create the rule
@@ -253,20 +150,47 @@ async function createInboxRule(accessToken, ruleOptions) {
253
150
  );
254
151
 
255
152
  if (response && response.id) {
153
+ let text = `Successfully created rule "${name}" with sequence ${ruleSequence}.`;
154
+ if (allWarnings.length > 0) {
155
+ text += `\n\nNotes:\n${allWarnings.map((w) => `- ${w}`).join('\n')}`;
156
+ }
157
+ if (!sequence) {
158
+ text +=
159
+ "\n\nTip: You can specify a 'sequence' parameter when creating rules to control their execution order. Lower sequence numbers run first.";
160
+ }
256
161
  return {
257
- success: true,
258
- message: `Successfully created rule "${name}" with sequence ${ruleSequence}.`,
259
- ruleId: response.id,
162
+ content: [{ type: 'text', text }],
260
163
  };
261
- } else {
164
+ }
165
+
166
+ return {
167
+ content: [
168
+ {
169
+ type: 'text',
170
+ text: "Failed to create rule. The server didn't return a rule ID.",
171
+ },
172
+ ],
173
+ };
174
+ } catch (error) {
175
+ if (error.message === 'Authentication required') {
262
176
  return {
263
- success: false,
264
- message: "Failed to create rule. The server didn't return a rule ID.",
177
+ content: [
178
+ {
179
+ type: 'text',
180
+ text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
181
+ },
182
+ ],
265
183
  };
266
184
  }
267
- } catch (error) {
268
- console.error(`Error creating rule: ${error.message}`);
269
- throw error;
185
+
186
+ return {
187
+ content: [
188
+ {
189
+ type: 'text',
190
+ text: `Error creating rule: ${error.message}`,
191
+ },
192
+ ],
193
+ };
270
194
  }
271
195
  }
272
196
 
package/rules/index.js CHANGED
@@ -3,8 +3,10 @@
3
3
  */
4
4
  const { handleListRules, getInboxRules } = require('./list');
5
5
  const handleCreateRule = require('./create');
6
+ const handleUpdateRule = require('./update');
6
7
  const { callGraphAPI } = require('../utils/graph-api');
7
8
  const { ensureAuthenticated } = require('../auth');
9
+ const { checkRateLimit } = require('../utils/safety');
8
10
 
9
11
  /**
10
12
  * Delete rule handler
@@ -14,6 +16,10 @@ const { ensureAuthenticated } = require('../auth');
14
16
  async function handleDeleteRule(args) {
15
17
  const { ruleName, ruleId } = args;
16
18
 
19
+ // Rate limit
20
+ const rateLimitError = checkRateLimit('manage-rules');
21
+ if (rateLimitError) return rateLimitError;
22
+
17
23
  if (!ruleName && !ruleId) {
18
24
  return {
19
25
  content: [
@@ -116,13 +122,9 @@ async function handleEditRuleSequence(args) {
116
122
  }
117
123
 
118
124
  try {
119
- // Get access token
120
125
  const accessToken = await ensureAuthenticated();
121
-
122
- // Get all rules
123
126
  const rules = await getInboxRules(accessToken);
124
127
 
125
- // Find the rule by name
126
128
  const rule = rules.find((r) => r.displayName === ruleName);
127
129
  if (!rule) {
128
130
  return {
@@ -135,14 +137,11 @@ async function handleEditRuleSequence(args) {
135
137
  };
136
138
  }
137
139
 
138
- // Update the rule sequence
139
- const _updateResult = await callGraphAPI(
140
+ await callGraphAPI(
140
141
  accessToken,
141
142
  'PATCH',
142
143
  `me/mailFolders/inbox/messageRules/${rule.id}`,
143
- {
144
- sequence: sequence,
145
- }
144
+ { sequence }
146
145
  );
147
146
 
148
147
  return {
@@ -181,7 +180,7 @@ const rulesTools = [
181
180
  {
182
181
  name: 'manage-rules',
183
182
  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.',
183
+ 'Manage inbox rules. action=list (default) lists rules. action=create creates a new rule with conditions, actions, and optional exceptions. action=update modifies an existing rule. action=reorder changes rule execution priority. action=delete removes a rule.',
185
184
  annotations: {
186
185
  title: 'Inbox Rules',
187
186
  readOnlyHint: false,
@@ -193,59 +192,190 @@ const rulesTools = [
193
192
  properties: {
194
193
  action: {
195
194
  type: 'string',
196
- enum: ['list', 'create', 'reorder', 'delete'],
195
+ enum: ['list', 'create', 'update', 'reorder', 'delete'],
197
196
  description: 'Action to perform (default: list)',
198
197
  },
199
- // list params
198
+
199
+ // === List params ===
200
200
  includeDetails: {
201
201
  type: 'boolean',
202
202
  description:
203
- 'Include detailed rule conditions and actions (action=list)',
203
+ 'Include detailed conditions, actions, and exceptions (action=list)',
204
204
  },
205
- // create params
205
+
206
+ // === Create + Update shared params ===
206
207
  name: {
207
208
  type: 'string',
208
- description: 'Name of the rule to create (action=create, required)',
209
+ description:
210
+ 'Rule name (action=create required, action=update to rename)',
211
+ },
212
+ dryRun: {
213
+ type: 'boolean',
214
+ description:
215
+ 'Preview rule without creating/updating (action=create, action=update)',
209
216
  },
217
+ isEnabled: {
218
+ type: 'boolean',
219
+ description:
220
+ 'Enable/disable rule (action=create default: true, action=update)',
221
+ },
222
+ sequence: {
223
+ type: 'number',
224
+ description:
225
+ 'Execution order, lower = higher priority (action=create default: auto, action=reorder required)',
226
+ },
227
+
228
+ // --- Conditions (comma-separated strings become OR arrays) ---
210
229
  fromAddresses: {
211
230
  type: 'string',
212
- description: 'Comma-separated sender email addresses (action=create)',
231
+ description:
232
+ 'Comma-separated sender emails to match (action=create/update)',
213
233
  },
214
234
  containsSubject: {
215
235
  type: 'string',
216
- description: 'Subject text the email must contain (action=create)',
236
+ description:
237
+ 'Comma-separated subject keywords (OR logic). e.g. "invoice, receipt, payment" (action=create/update)',
238
+ },
239
+ bodyContains: {
240
+ type: 'string',
241
+ description:
242
+ 'Comma-separated body text keywords (OR logic) (action=create/update)',
243
+ },
244
+ bodyOrSubjectContains: {
245
+ type: 'string',
246
+ description:
247
+ 'Comma-separated keywords matching body OR subject (OR logic) (action=create/update)',
248
+ },
249
+ senderContains: {
250
+ type: 'string',
251
+ description:
252
+ 'Comma-separated partial sender matches (action=create/update)',
253
+ },
254
+ recipientContains: {
255
+ type: 'string',
256
+ description:
257
+ 'Comma-separated partial recipient matches (action=create/update)',
258
+ },
259
+ sentToAddresses: {
260
+ type: 'string',
261
+ description:
262
+ 'Comma-separated recipient emails to match (action=create/update)',
217
263
  },
218
264
  hasAttachments: {
219
265
  type: 'boolean',
220
- description: 'Apply to emails with attachments (action=create)',
266
+ description: 'Match emails with attachments (action=create/update)',
267
+ },
268
+ importance: {
269
+ type: 'string',
270
+ enum: ['low', 'normal', 'high'],
271
+ description:
272
+ 'Match emails with this importance (action=create/update)',
221
273
  },
274
+ sensitivity: {
275
+ type: 'string',
276
+ enum: ['normal', 'personal', 'private', 'confidential'],
277
+ description:
278
+ 'Match emails with this sensitivity (action=create/update)',
279
+ },
280
+ sentToMe: {
281
+ type: 'boolean',
282
+ description: 'Match emails sent to me (action=create/update)',
283
+ },
284
+ sentOnlyToMe: {
285
+ type: 'boolean',
286
+ description:
287
+ 'Match emails where I am the only recipient (action=create/update)',
288
+ },
289
+ sentCcMe: {
290
+ type: 'boolean',
291
+ description: 'Match emails where I am in CC (action=create/update)',
292
+ },
293
+ isAutomaticReply: {
294
+ type: 'boolean',
295
+ description: 'Match automatic reply emails (action=create/update)',
296
+ },
297
+
298
+ // --- Actions ---
222
299
  moveToFolder: {
223
300
  type: 'string',
224
- description: 'Folder to move matching emails to (action=create)',
301
+ description:
302
+ 'Folder name to move matching emails to (action=create/update)',
303
+ },
304
+ copyToFolder: {
305
+ type: 'string',
306
+ description:
307
+ 'Folder name to copy matching emails to (action=create/update)',
225
308
  },
226
309
  markAsRead: {
227
310
  type: 'boolean',
228
- description: 'Mark matching emails as read (action=create)',
311
+ description: 'Mark matching emails as read (action=create/update)',
229
312
  },
230
- isEnabled: {
313
+ markImportance: {
314
+ type: 'string',
315
+ enum: ['low', 'normal', 'high'],
316
+ description:
317
+ 'Set importance on matching emails (action=create/update)',
318
+ },
319
+ forwardTo: {
320
+ type: 'string',
321
+ description:
322
+ 'Comma-separated emails to forward matching messages to (action=create/update)',
323
+ },
324
+ redirectTo: {
325
+ type: 'string',
326
+ description:
327
+ 'Comma-separated emails to redirect matching messages to (action=create/update)',
328
+ },
329
+ assignCategories: {
330
+ type: 'string',
331
+ description:
332
+ 'Comma-separated Outlook categories to assign (action=create/update)',
333
+ },
334
+ stopProcessingRules: {
231
335
  type: 'boolean',
232
336
  description:
233
- 'Enable rule after creation, default: true (action=create)',
337
+ 'Stop evaluating subsequent rules (action=create/update)',
234
338
  },
235
- sequence: {
236
- type: 'number',
339
+ deleteMessage: {
340
+ type: 'boolean',
237
341
  description:
238
- 'Execution order, lower numbers run first (action=create default: 100, action=reorder required)',
342
+ 'Move matching emails to Deleted Items (action=create/update)',
239
343
  },
240
- // reorder params
241
- ruleName: {
344
+
345
+ // --- Exceptions (rule skipped when these match) ---
346
+ exceptFromAddresses: {
347
+ type: 'string',
348
+ description:
349
+ 'Comma-separated sender emails to exclude (action=create/update)',
350
+ },
351
+ exceptSubjectContains: {
352
+ type: 'string',
353
+ description:
354
+ 'Comma-separated subject keywords to exclude (action=create/update)',
355
+ },
356
+ exceptSenderContains: {
357
+ type: 'string',
358
+ description:
359
+ 'Comma-separated partial sender matches to exclude (action=create/update)',
360
+ },
361
+ exceptBodyContains: {
242
362
  type: 'string',
243
363
  description:
244
- 'Name of the rule (action=reorder required, action=delete alternative to ruleId)',
364
+ 'Comma-separated body keywords to exclude (action=create/update)',
365
+ },
366
+ exceptHasAttachments: {
367
+ type: 'boolean',
368
+ description: 'Exclude emails with attachments (action=create/update)',
369
+ },
370
+
371
+ // === Update + Reorder + Delete params ===
372
+ ruleName: {
373
+ type: 'string',
374
+ description: 'Name of existing rule (action=update/reorder/delete)',
245
375
  },
246
376
  ruleId: {
247
377
  type: 'string',
248
- description: 'ID of the rule to delete (action=delete)',
378
+ description: 'ID of existing rule (action=update/delete)',
249
379
  },
250
380
  },
251
381
  required: [],
@@ -255,6 +385,8 @@ const rulesTools = [
255
385
  switch (action) {
256
386
  case 'create':
257
387
  return handleCreateRule(args);
388
+ case 'update':
389
+ return handleUpdateRule(args);
258
390
  case 'reorder':
259
391
  return handleEditRuleSequence(args);
260
392
  case 'delete':
@@ -271,6 +403,7 @@ module.exports = {
271
403
  rulesTools,
272
404
  handleListRules,
273
405
  handleCreateRule,
406
+ handleUpdateRule,
274
407
  handleEditRuleSequence,
275
408
  handleDeleteRule,
276
409
  };