@littlebearapps/outlook-assistant 3.5.2 → 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/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
  };
package/rules/list.js CHANGED
@@ -13,13 +13,8 @@ async function handleListRules(args) {
13
13
  const includeDetails = args.includeDetails === true;
14
14
 
15
15
  try {
16
- // Get access token
17
16
  const accessToken = await ensureAuthenticated();
18
-
19
- // Get all inbox rules
20
17
  const rules = await getInboxRules(accessToken);
21
-
22
- // Format the rules based on detail level
23
18
  const formattedRules = formatRulesList(rules, includeDetails);
24
19
 
25
20
  return {
@@ -36,7 +31,7 @@ async function handleListRules(args) {
36
31
  content: [
37
32
  {
38
33
  type: 'text',
39
- text: "Authentication required. Please use the 'authenticate' tool first.",
34
+ text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
40
35
  },
41
36
  ],
42
37
  };
@@ -82,45 +77,43 @@ async function getInboxRules(accessToken) {
82
77
  */
83
78
  function formatRulesList(rules, includeDetails) {
84
79
  if (!rules || rules.length === 0) {
85
- 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
+ return 'No inbox rules found.\n\nTip: You can create rules using manage-rules with action=create. Rules are processed in order of their sequence number (lower numbers are processed first).';
86
81
  }
87
82
 
88
- // Sort rules by sequence to show execution order
89
83
  const sortedRules = [...rules].sort((a, b) => {
90
84
  return (a.sequence || 9999) - (b.sequence || 9999);
91
85
  });
92
86
 
93
- // Format rules based on detail level
94
87
  if (includeDetails) {
95
- // Detailed format
96
88
  const detailedRules = sortedRules.map((rule) => {
97
- // Format rule header with sequence
98
89
  let ruleText = `[${rule.sequence || 'N/A'}] ${rule.displayName}${rule.isEnabled ? '' : ' (Disabled)'}`;
99
90
 
100
- // Format conditions
101
91
  const conditions = formatRuleConditions(rule);
102
92
  if (conditions) {
103
93
  ruleText += `\n Conditions: ${conditions}`;
104
94
  }
105
95
 
106
- // Format actions
107
96
  const actions = formatRuleActions(rule);
108
97
  if (actions) {
109
98
  ruleText += `\n Actions: ${actions}`;
110
99
  }
111
100
 
101
+ const exceptions = formatRuleExceptions(rule);
102
+ if (exceptions) {
103
+ ruleText += `\n Exceptions: ${exceptions}`;
104
+ }
105
+
112
106
  return ruleText;
113
107
  });
114
108
 
115
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 manage-rules with action=reorder.`;
116
- } else {
117
- // Simple format
118
- const simpleRules = sortedRules.map((rule) => {
119
- return `[${rule.sequence || 'N/A'}] ${rule.displayName}${rule.isEnabled ? '' : ' (Disabled)'}`;
120
- });
121
-
122
- 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.`;
123
110
  }
111
+
112
+ const simpleRules = sortedRules.map((rule) => {
113
+ return `[${rule.sequence || 'N/A'}] ${rule.displayName}${rule.isEnabled ? '' : ' (Disabled)'}`;
114
+ });
115
+
116
+ return `Found ${rules.length} inbox rules (sorted by execution order):\n\n${simpleRules.join('\n')}\n\nTip: Use manage-rules with includeDetails=true to see conditions, actions, and exceptions for each rule.`;
124
117
  }
125
118
 
126
119
  /**
@@ -130,37 +123,76 @@ function formatRulesList(rules, includeDetails) {
130
123
  */
131
124
  function formatRuleConditions(rule) {
132
125
  const conditions = [];
126
+ const c = rule.conditions;
127
+ if (!c) return '';
133
128
 
134
- // From addresses
135
- if (rule.conditions?.fromAddresses?.length > 0) {
136
- const senders = rule.conditions.fromAddresses
137
- .map((addr) => addr.emailAddress.address)
129
+ // Recipient-based
130
+ if (c.fromAddresses?.length > 0) {
131
+ const senders = c.fromAddresses
132
+ .map((addr) => addr.emailAddress?.address)
138
133
  .join(', ');
139
134
  conditions.push(`From: ${senders}`);
140
135
  }
136
+ if (c.sentToAddresses?.length > 0) {
137
+ const recipients = c.sentToAddresses
138
+ .map((addr) => addr.emailAddress?.address)
139
+ .join(', ');
140
+ conditions.push(`Sent to: ${recipients}`);
141
+ }
141
142
 
142
- // Subject contains
143
- if (rule.conditions?.subjectContains?.length > 0) {
143
+ // String collections
144
+ if (c.subjectContains?.length > 0) {
145
+ conditions.push(`Subject contains: "${c.subjectContains.join('", "')}"`);
146
+ }
147
+ if (c.bodyContains?.length > 0) {
148
+ conditions.push(`Body contains: "${c.bodyContains.join('", "')}"`);
149
+ }
150
+ if (c.bodyOrSubjectContains?.length > 0) {
144
151
  conditions.push(
145
- `Subject contains: "${rule.conditions.subjectContains.join(', ')}"`
152
+ `Body/subject contains: "${c.bodyOrSubjectContains.join('", "')}"`
146
153
  );
147
154
  }
148
-
149
- // Contains body text
150
- if (rule.conditions?.bodyContains?.length > 0) {
155
+ if (c.senderContains?.length > 0) {
156
+ conditions.push(`Sender contains: "${c.senderContains.join('", "')}"`);
157
+ }
158
+ if (c.recipientContains?.length > 0) {
151
159
  conditions.push(
152
- `Body contains: "${rule.conditions.bodyContains.join(', ')}"`
160
+ `Recipient contains: "${c.recipientContains.join('", "')}"`
153
161
  );
154
162
  }
163
+ if (c.headerContains?.length > 0) {
164
+ conditions.push(`Header contains: "${c.headerContains.join('", "')}"`);
165
+ }
155
166
 
156
- // Has attachment
157
- if (rule.conditions?.hasAttachment === true) {
158
- conditions.push('Has attachment');
167
+ // Booleans
168
+ if (c.hasAttachment === true) conditions.push('Has attachment');
169
+ if (c.sentToMe === true) conditions.push('Sent to me');
170
+ if (c.sentOnlyToMe === true) conditions.push('Sent only to me');
171
+ if (c.sentCcMe === true) conditions.push('I am in CC');
172
+ if (c.sentToOrCcMe === true) conditions.push('Sent to me or CC');
173
+ if (c.isAutomaticReply === true) conditions.push('Is automatic reply');
174
+ if (c.isAutomaticForward === true) conditions.push('Is automatic forward');
175
+ if (c.isMeetingRequest === true) conditions.push('Is meeting request');
176
+ if (c.isMeetingResponse === true) conditions.push('Is meeting response');
177
+ if (c.isReadReceipt === true) conditions.push('Is read receipt');
178
+ if (c.isEncrypted === true) conditions.push('Is encrypted');
179
+ if (c.notSentToMe === true) conditions.push('Not sent to me');
180
+
181
+ // Enums
182
+ if (c.importance) conditions.push(`Importance: ${c.importance}`);
183
+ if (c.sensitivity) conditions.push(`Sensitivity: ${c.sensitivity}`);
184
+ if (c.messageActionFlag) conditions.push(`Flag: ${c.messageActionFlag}`);
185
+
186
+ // Categories
187
+ if (c.categories?.length > 0) {
188
+ conditions.push(`Categories: ${c.categories.join(', ')}`);
159
189
  }
160
190
 
161
- // Importance
162
- if (rule.conditions?.importance) {
163
- conditions.push(`Importance: ${rule.conditions.importance}`);
191
+ // Size range
192
+ if (c.withinSizeRange) {
193
+ const min = c.withinSizeRange.minimumSize || 0;
194
+ const max = c.withinSizeRange.maximumSize || '∞';
195
+ conditions.push(`Size: ${min}–${max} bytes`);
164
196
  }
165
197
 
166
198
  return conditions.join('; ');
@@ -173,43 +205,93 @@ function formatRuleConditions(rule) {
173
205
  */
174
206
  function formatRuleActions(rule) {
175
207
  const actions = [];
208
+ const a = rule.actions;
209
+ if (!a) return '';
176
210
 
177
- // Move to folder
178
- if (rule.actions?.moveToFolder) {
179
- actions.push(`Move to folder: ${rule.actions.moveToFolder}`);
180
- }
211
+ if (a.moveToFolder) actions.push(`Move to folder: ${a.moveToFolder}`);
212
+ if (a.copyToFolder) actions.push(`Copy to folder: ${a.copyToFolder}`);
213
+ if (a.markAsRead === true) actions.push('Mark as read');
214
+ if (a.markImportance) actions.push(`Mark importance: ${a.markImportance}`);
181
215
 
182
- // Copy to folder
183
- if (rule.actions?.copyToFolder) {
184
- actions.push(`Copy to folder: ${rule.actions.copyToFolder}`);
185
- }
186
-
187
- // Mark as read
188
- if (rule.actions?.markAsRead === true) {
189
- actions.push('Mark as read');
216
+ if (a.forwardTo?.length > 0) {
217
+ const recipients = a.forwardTo
218
+ .map((r) => r.emailAddress?.address)
219
+ .join(', ');
220
+ actions.push(`Forward to: ${recipients}`);
190
221
  }
191
-
192
- // Mark importance
193
- if (rule.actions?.markImportance) {
194
- actions.push(`Mark importance: ${rule.actions.markImportance}`);
222
+ if (a.forwardAsAttachmentTo?.length > 0) {
223
+ const recipients = a.forwardAsAttachmentTo
224
+ .map((r) => r.emailAddress?.address)
225
+ .join(', ');
226
+ actions.push(`Forward as attachment to: ${recipients}`);
195
227
  }
196
-
197
- // Forward
198
- if (rule.actions?.forwardTo?.length > 0) {
199
- const recipients = rule.actions.forwardTo
200
- .map((r) => r.emailAddress.address)
228
+ if (a.redirectTo?.length > 0) {
229
+ const recipients = a.redirectTo
230
+ .map((r) => r.emailAddress?.address)
201
231
  .join(', ');
202
- actions.push(`Forward to: ${recipients}`);
232
+ actions.push(`Redirect to: ${recipients}`);
203
233
  }
204
234
 
205
- // Delete
206
- if (rule.actions?.delete === true) {
207
- actions.push('Delete');
235
+ if (a.assignCategories?.length > 0) {
236
+ actions.push(`Assign categories: ${a.assignCategories.join(', ')}`);
208
237
  }
238
+ if (a.stopProcessingRules === true) actions.push('Stop processing rules');
239
+ if (a.delete === true) actions.push('Delete (move to Deleted Items)');
240
+ if (a.permanentDelete === true) actions.push('Permanently delete');
209
241
 
210
242
  return actions.join('; ');
211
243
  }
212
244
 
245
+ /**
246
+ * Format rule exceptions for display
247
+ * @param {object} rule - Rule object
248
+ * @returns {string} - Formatted exceptions
249
+ */
250
+ function formatRuleExceptions(rule) {
251
+ const parts = [];
252
+ const e = rule.exceptions;
253
+ if (!e) return '';
254
+
255
+ if (e.fromAddresses?.length > 0) {
256
+ const senders = e.fromAddresses
257
+ .map((addr) => addr.emailAddress?.address)
258
+ .join(', ');
259
+ parts.push(`From: ${senders}`);
260
+ }
261
+ if (e.sentToAddresses?.length > 0) {
262
+ const recipients = e.sentToAddresses
263
+ .map((addr) => addr.emailAddress?.address)
264
+ .join(', ');
265
+ parts.push(`Sent to: ${recipients}`);
266
+ }
267
+ if (e.subjectContains?.length > 0) {
268
+ parts.push(`Subject contains: "${e.subjectContains.join('", "')}"`);
269
+ }
270
+ if (e.bodyContains?.length > 0) {
271
+ parts.push(`Body contains: "${e.bodyContains.join('", "')}"`);
272
+ }
273
+ if (e.bodyOrSubjectContains?.length > 0) {
274
+ parts.push(
275
+ `Body/subject contains: "${e.bodyOrSubjectContains.join('", "')}"`
276
+ );
277
+ }
278
+ if (e.senderContains?.length > 0) {
279
+ parts.push(`Sender contains: "${e.senderContains.join('", "')}"`);
280
+ }
281
+ if (e.recipientContains?.length > 0) {
282
+ parts.push(`Recipient contains: "${e.recipientContains.join('", "')}"`);
283
+ }
284
+ if (e.hasAttachment === true) parts.push('Has attachment');
285
+ if (e.importance) parts.push(`Importance: ${e.importance}`);
286
+ if (e.sensitivity) parts.push(`Sensitivity: ${e.sensitivity}`);
287
+ if (e.sentToMe === true) parts.push('Sent to me');
288
+ if (e.sentOnlyToMe === true) parts.push('Sent only to me');
289
+ if (e.sentCcMe === true) parts.push('I am in CC');
290
+ if (e.isAutomaticReply === true) parts.push('Is automatic reply');
291
+
292
+ return parts.join('; ');
293
+ }
294
+
213
295
  module.exports = {
214
296
  handleListRules,
215
297
  getInboxRules,