@littlebearapps/outlook-assistant 3.6.0 → 3.7.1

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/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,
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Shared rule builder utilities for creating and updating mail rules.
3
+ * Converts flat MCP tool parameters into Microsoft Graph API rule objects.
4
+ */
5
+ const { getFolderIdByName } = require('../email/folder-utils');
6
+ const { checkRecipientAllowlist } = require('../utils/safety');
7
+
8
+ const VALID_IMPORTANCE = ['low', 'normal', 'high'];
9
+ const VALID_SENSITIVITY = ['normal', 'personal', 'private', 'confidential'];
10
+
11
+ /**
12
+ * Parse a comma-separated string into a trimmed, non-empty array.
13
+ * @param {string} str - Comma-separated values
14
+ * @returns {string[]}
15
+ */
16
+ function parseCommaSeparated(str) {
17
+ if (!str) return [];
18
+ return str
19
+ .split(',')
20
+ .map((s) => s.trim())
21
+ .filter((s) => s.length > 0);
22
+ }
23
+
24
+ /**
25
+ * Convert a comma-separated email string into Graph API recipient objects.
26
+ * @param {string} emailString - Comma-separated email addresses
27
+ * @returns {Array<{emailAddress: {address: string}}>}
28
+ */
29
+ function formatRecipientObjects(emailString) {
30
+ return parseCommaSeparated(emailString).map((email) => ({
31
+ emailAddress: { address: email },
32
+ }));
33
+ }
34
+
35
+ /**
36
+ * Build a Graph API conditions object from flat tool parameters.
37
+ * @param {object} args - Tool arguments
38
+ * @returns {{ conditions: object, warnings: string[] }}
39
+ */
40
+ function buildConditions(args) {
41
+ const conditions = {};
42
+ const warnings = [];
43
+
44
+ // String collections (comma-separated → array with OR logic)
45
+ if (args.containsSubject) {
46
+ conditions.subjectContains = parseCommaSeparated(args.containsSubject);
47
+ }
48
+ if (args.bodyContains) {
49
+ conditions.bodyContains = parseCommaSeparated(args.bodyContains);
50
+ }
51
+ if (args.bodyOrSubjectContains) {
52
+ conditions.bodyOrSubjectContains = parseCommaSeparated(
53
+ args.bodyOrSubjectContains
54
+ );
55
+ }
56
+ if (args.senderContains) {
57
+ conditions.senderContains = parseCommaSeparated(args.senderContains);
58
+ }
59
+ if (args.recipientContains) {
60
+ conditions.recipientContains = parseCommaSeparated(args.recipientContains);
61
+ }
62
+
63
+ // Recipient collections (comma-separated emails → recipient objects)
64
+ if (args.fromAddresses) {
65
+ const recipients = formatRecipientObjects(args.fromAddresses);
66
+ if (recipients.length > 0) {
67
+ conditions.fromAddresses = recipients;
68
+ }
69
+ }
70
+ if (args.sentToAddresses) {
71
+ const recipients = formatRecipientObjects(args.sentToAddresses);
72
+ if (recipients.length > 0) {
73
+ conditions.sentToAddresses = recipients;
74
+ }
75
+ }
76
+
77
+ // Boolean conditions
78
+ if (args.hasAttachments === true) conditions.hasAttachment = true;
79
+ if (args.sentToMe === true) conditions.sentToMe = true;
80
+ if (args.sentOnlyToMe === true) conditions.sentOnlyToMe = true;
81
+ if (args.sentCcMe === true) conditions.sentCcMe = true;
82
+ if (args.isAutomaticReply === true) conditions.isAutomaticReply = true;
83
+
84
+ // Enum conditions
85
+ if (args.importance) {
86
+ if (!VALID_IMPORTANCE.includes(args.importance)) {
87
+ warnings.push(
88
+ `Invalid importance "${args.importance}". Must be: ${VALID_IMPORTANCE.join(', ')}`
89
+ );
90
+ } else {
91
+ conditions.importance = args.importance;
92
+ }
93
+ }
94
+ if (args.sensitivity) {
95
+ if (!VALID_SENSITIVITY.includes(args.sensitivity)) {
96
+ warnings.push(
97
+ `Invalid sensitivity "${args.sensitivity}". Must be: ${VALID_SENSITIVITY.join(', ')}`
98
+ );
99
+ } else {
100
+ conditions.sensitivity = args.sensitivity;
101
+ }
102
+ }
103
+
104
+ return { conditions, warnings };
105
+ }
106
+
107
+ /**
108
+ * Build a Graph API actions object from flat tool parameters.
109
+ * Async because folder resolution requires API calls.
110
+ * @param {object} args - Tool arguments
111
+ * @param {string} accessToken - Graph API access token
112
+ * @returns {Promise<{ actions: object, warnings: string[] }>}
113
+ */
114
+ async function buildActions(args, accessToken) {
115
+ const actions = {};
116
+ const warnings = [];
117
+
118
+ // Folder-based actions (name → ID resolution)
119
+ if (args.moveToFolder) {
120
+ const folderId = await getFolderIdByName(accessToken, args.moveToFolder);
121
+ if (!folderId) {
122
+ warnings.push(`Target folder "${args.moveToFolder}" not found.`);
123
+ } else {
124
+ actions.moveToFolder = folderId;
125
+ }
126
+ }
127
+ if (args.copyToFolder) {
128
+ const folderId = await getFolderIdByName(accessToken, args.copyToFolder);
129
+ if (!folderId) {
130
+ warnings.push(`Copy-to folder "${args.copyToFolder}" not found.`);
131
+ } else {
132
+ actions.copyToFolder = folderId;
133
+ }
134
+ }
135
+
136
+ // Boolean actions
137
+ if (args.markAsRead === true) actions.markAsRead = true;
138
+ if (args.stopProcessingRules === true) actions.stopProcessingRules = true;
139
+ if (args.deleteMessage === true) {
140
+ actions.delete = true;
141
+ warnings.push('This rule will move matching messages to Deleted Items.');
142
+ }
143
+
144
+ // Enum actions
145
+ if (args.markImportance) {
146
+ if (!VALID_IMPORTANCE.includes(args.markImportance)) {
147
+ warnings.push(
148
+ `Invalid markImportance "${args.markImportance}". Must be: ${VALID_IMPORTANCE.join(', ')}`
149
+ );
150
+ } else {
151
+ actions.markImportance = args.markImportance;
152
+ }
153
+ }
154
+
155
+ // Recipient-based actions (comma-separated emails → recipient objects)
156
+ if (args.forwardTo) {
157
+ const recipients = formatRecipientObjects(args.forwardTo);
158
+ if (recipients.length > 0) {
159
+ // Check allowlist if configured
160
+ const allowlistError = checkRecipientAllowlist(recipients);
161
+ if (allowlistError) {
162
+ warnings.push(
163
+ `Forward recipients blocked by allowlist: ${args.forwardTo}`
164
+ );
165
+ } else {
166
+ actions.forwardTo = recipients;
167
+ warnings.push(
168
+ `This rule will forward all matching emails to: ${args.forwardTo}. Verify these addresses are correct.`
169
+ );
170
+ }
171
+ }
172
+ }
173
+ if (args.redirectTo) {
174
+ const recipients = formatRecipientObjects(args.redirectTo);
175
+ if (recipients.length > 0) {
176
+ const allowlistError = checkRecipientAllowlist(recipients);
177
+ if (allowlistError) {
178
+ warnings.push(
179
+ `Redirect recipients blocked by allowlist: ${args.redirectTo}`
180
+ );
181
+ } else {
182
+ actions.redirectTo = recipients;
183
+ warnings.push(
184
+ `This rule will redirect all matching emails to: ${args.redirectTo}. The original sender will appear as the sender.`
185
+ );
186
+ }
187
+ }
188
+ }
189
+
190
+ // Category actions
191
+ if (args.assignCategories) {
192
+ actions.assignCategories = parseCommaSeparated(args.assignCategories);
193
+ }
194
+
195
+ return { actions, warnings };
196
+ }
197
+
198
+ /**
199
+ * Build a Graph API exceptions object from except* prefixed parameters.
200
+ * @param {object} args - Tool arguments
201
+ * @returns {{ exceptions: object, warnings: string[] }}
202
+ */
203
+ function buildExceptions(args) {
204
+ const exceptions = {};
205
+ const warnings = [];
206
+
207
+ if (args.exceptFromAddresses) {
208
+ const recipients = formatRecipientObjects(args.exceptFromAddresses);
209
+ if (recipients.length > 0) {
210
+ exceptions.fromAddresses = recipients;
211
+ }
212
+ }
213
+ if (args.exceptSubjectContains) {
214
+ exceptions.subjectContains = parseCommaSeparated(
215
+ args.exceptSubjectContains
216
+ );
217
+ }
218
+ if (args.exceptSenderContains) {
219
+ exceptions.senderContains = parseCommaSeparated(args.exceptSenderContains);
220
+ }
221
+ if (args.exceptBodyContains) {
222
+ exceptions.bodyContains = parseCommaSeparated(args.exceptBodyContains);
223
+ }
224
+ if (args.exceptHasAttachments === true) {
225
+ exceptions.hasAttachment = true;
226
+ }
227
+
228
+ return { exceptions, warnings };
229
+ }
230
+
231
+ /**
232
+ * Check if any condition was provided in the args.
233
+ * @param {object} args - Tool arguments
234
+ * @returns {boolean}
235
+ */
236
+ function hasAnyCondition(args) {
237
+ return Boolean(
238
+ args.fromAddresses ||
239
+ args.containsSubject ||
240
+ args.hasAttachments === true ||
241
+ args.bodyContains ||
242
+ args.bodyOrSubjectContains ||
243
+ args.senderContains ||
244
+ args.recipientContains ||
245
+ args.sentToAddresses ||
246
+ args.importance ||
247
+ args.sensitivity ||
248
+ args.sentToMe === true ||
249
+ args.sentOnlyToMe === true ||
250
+ args.sentCcMe === true ||
251
+ args.isAutomaticReply === true
252
+ );
253
+ }
254
+
255
+ /**
256
+ * Check if any action was provided in the args.
257
+ * @param {object} args - Tool arguments
258
+ * @returns {boolean}
259
+ */
260
+ function hasAnyAction(args) {
261
+ return Boolean(
262
+ args.moveToFolder ||
263
+ args.copyToFolder ||
264
+ args.markAsRead === true ||
265
+ args.markImportance ||
266
+ args.forwardTo ||
267
+ args.redirectTo ||
268
+ args.assignCategories ||
269
+ args.stopProcessingRules === true ||
270
+ args.deleteMessage === true
271
+ );
272
+ }
273
+
274
+ module.exports = {
275
+ VALID_IMPORTANCE,
276
+ VALID_SENSITIVITY,
277
+ parseCommaSeparated,
278
+ formatRecipientObjects,
279
+ buildConditions,
280
+ buildActions,
281
+ buildExceptions,
282
+ hasAnyCondition,
283
+ hasAnyAction,
284
+ };