@littlebearapps/outlook-assistant 3.12.1 → 3.14.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/.env.example +27 -3
- package/README.md +108 -33
- package/advanced/index.js +44 -174
- package/auth/auth-errors.js +23 -1
- package/auth/client-config.js +142 -0
- package/auth/index.js +4 -2
- package/auth/oauth-server.js +12 -2
- package/auth/token-manager.js +7 -3
- package/auth/token-storage.js +46 -33
- package/auth/tools.js +223 -93
- package/calendar/attendees.js +36 -0
- package/calendar/cancel.js +9 -25
- package/calendar/create.js +42 -48
- package/calendar/decline.js +10 -25
- package/calendar/delete.js +10 -25
- package/calendar/index.js +20 -37
- package/calendar/list.js +4 -16
- package/calendar/preview.js +335 -0
- package/calendar/update.js +42 -86
- package/categories/index.js +59 -264
- package/config.js +36 -2
- package/contacts/index.js +72 -128
- package/email/attachments.js +42 -124
- package/email/conversations.js +44 -78
- package/email/delta.js +10 -34
- package/email/draft.js +140 -96
- package/email/export.js +141 -110
- package/email/folder-utils.js +3 -2
- package/email/headers.js +11 -49
- package/email/index.js +85 -109
- package/email/list.js +4 -17
- package/email/mail-tips.js +86 -57
- package/email/mark-as-read.js +13 -49
- package/email/mime.js +14 -49
- package/email/read.js +16 -50
- package/email/search.js +46 -86
- package/email/send.js +82 -48
- package/folder/create.js +6 -25
- package/folder/delete.js +117 -38
- package/folder/index.js +17 -16
- package/folder/list.js +5 -17
- package/folder/move.js +13 -42
- package/folder/resolve.js +11 -6
- package/folder/stats.js +6 -20
- package/index.js +23 -45
- package/llms-install.md +31 -7
- package/llms.txt +19 -10
- package/outlook-auth-server.js +10 -3
- package/package.json +6 -2
- package/request-handler.js +217 -116
- package/rules/create.js +27 -70
- package/rules/index.js +30 -92
- package/rules/list.js +5 -17
- package/rules/rule-builder.js +57 -20
- package/rules/update.js +26 -60
- package/server.js +37 -0
- package/settings/index.js +142 -143
- package/tools.js +30 -0
- package/utils/field-presets.js +4 -2
- package/utils/graph-api.js +65 -22
- package/utils/logger.js +251 -0
- package/utils/mock-data.js +91 -2
- package/utils/read-only.js +59 -0
- package/utils/response-formatter.js +54 -15
- package/utils/risk-classes.js +324 -0
- package/utils/safe-write.js +372 -6
- package/utils/safety.js +109 -25
- package/utils/server-instructions.js +62 -0
- package/utils/tool-error.js +33 -0
package/rules/create.js
CHANGED
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
*/
|
|
4
4
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
5
5
|
const { ensureAuthenticated } = require('../auth');
|
|
6
|
-
const { checkRateLimit } = require('../utils/safety');
|
|
7
|
-
const { formatRuleDryRunPreview } = require('../utils/safety');
|
|
6
|
+
const { checkRateLimit, formatRuleDryRunPreview } = require('../utils/safety');
|
|
8
7
|
const { getInboxRules } = require('./list');
|
|
9
8
|
const {
|
|
10
9
|
buildConditions,
|
|
11
10
|
buildActions,
|
|
12
11
|
buildExceptions,
|
|
12
|
+
checkRuleRecipients,
|
|
13
13
|
hasAnyCondition,
|
|
14
14
|
hasAnyAction,
|
|
15
15
|
} = require('./rule-builder');
|
|
16
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Create rule handler
|
|
@@ -22,55 +23,31 @@ const {
|
|
|
22
23
|
async function handleCreateRule(args) {
|
|
23
24
|
const { name, isEnabled = true, sequence, dryRun } = args;
|
|
24
25
|
|
|
25
|
-
// Rate limit rule creation
|
|
26
|
-
const rateLimitError = checkRateLimit('manage-rules');
|
|
27
|
-
if (rateLimitError) return rateLimitError;
|
|
28
|
-
|
|
29
26
|
// Validate sequence parameter
|
|
30
27
|
if (sequence !== undefined && (isNaN(sequence) || sequence < 1)) {
|
|
31
|
-
return
|
|
32
|
-
content: [
|
|
33
|
-
{
|
|
34
|
-
type: 'text',
|
|
35
|
-
text: 'Sequence must be a positive number greater than zero.',
|
|
36
|
-
},
|
|
37
|
-
],
|
|
38
|
-
};
|
|
28
|
+
return toolError('Sequence must be a positive number greater than zero.');
|
|
39
29
|
}
|
|
40
30
|
|
|
41
31
|
if (!name) {
|
|
42
|
-
return
|
|
43
|
-
content: [
|
|
44
|
-
{
|
|
45
|
-
type: 'text',
|
|
46
|
-
text: 'Rule name is required.',
|
|
47
|
-
},
|
|
48
|
-
],
|
|
49
|
-
};
|
|
32
|
+
return toolError('Rule name is required.');
|
|
50
33
|
}
|
|
51
34
|
|
|
52
35
|
if (!hasAnyCondition(args)) {
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
type: 'text',
|
|
57
|
-
text: 'At least one condition is required. Available conditions: fromAddresses, containsSubject, bodyContains, bodyOrSubjectContains, senderContains, recipientContains, sentToAddresses, hasAttachments, importance, sensitivity, sentToMe, sentOnlyToMe, sentCcMe, isAutomaticReply.',
|
|
58
|
-
},
|
|
59
|
-
],
|
|
60
|
-
};
|
|
36
|
+
return toolError(
|
|
37
|
+
'At least one condition is required. Available conditions: fromAddresses, containsSubject, bodyContains, bodyOrSubjectContains, senderContains, recipientContains, sentToAddresses, hasAttachments, importance, sensitivity, sentToMe, sentOnlyToMe, sentCcMe, isAutomaticReply.'
|
|
38
|
+
);
|
|
61
39
|
}
|
|
62
40
|
|
|
63
41
|
if (!hasAnyAction(args)) {
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
type: 'text',
|
|
68
|
-
text: 'At least one action is required. Available actions: moveToFolder, copyToFolder, markAsRead, markImportance, forwardTo, redirectTo, assignCategories, stopProcessingRules, deleteMessage.',
|
|
69
|
-
},
|
|
70
|
-
],
|
|
71
|
-
};
|
|
42
|
+
return toolError(
|
|
43
|
+
'At least one action is required. Available actions: moveToFolder, copyToFolder, markAsRead, markImportance, forwardTo, redirectTo, assignCategories, stopProcessingRules, deleteMessage.'
|
|
44
|
+
);
|
|
72
45
|
}
|
|
73
46
|
|
|
47
|
+
// Refuse the whole rule if the allowlist blocks any forwarding (#273)
|
|
48
|
+
const recipientError = checkRuleRecipients(args, { dryRun });
|
|
49
|
+
if (recipientError) return recipientError;
|
|
50
|
+
|
|
74
51
|
try {
|
|
75
52
|
const accessToken = await ensureAuthenticated();
|
|
76
53
|
|
|
@@ -87,14 +64,9 @@ async function handleCreateRule(args) {
|
|
|
87
64
|
// Check for fatal warnings (folder not found = no valid action)
|
|
88
65
|
const folderNotFound = actWarnings.some((w) => w.includes('not found'));
|
|
89
66
|
if (folderNotFound && Object.keys(actions).length === 0) {
|
|
90
|
-
return
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
type: 'text',
|
|
94
|
-
text: actWarnings.filter((w) => w.includes('not found')).join('\n'),
|
|
95
|
-
},
|
|
96
|
-
],
|
|
97
|
-
};
|
|
67
|
+
return toolError(
|
|
68
|
+
actWarnings.filter((w) => w.includes('not found')).join('\n')
|
|
69
|
+
);
|
|
98
70
|
}
|
|
99
71
|
|
|
100
72
|
// Determine sequence
|
|
@@ -141,6 +113,10 @@ async function handleCreateRule(args) {
|
|
|
141
113
|
};
|
|
142
114
|
}
|
|
143
115
|
|
|
116
|
+
// Rate limit only real writes, so a dry run never uses up a slot (#273)
|
|
117
|
+
const rateLimitError = checkRateLimit('manage-rules');
|
|
118
|
+
if (rateLimitError) return rateLimitError;
|
|
119
|
+
|
|
144
120
|
// Create the rule
|
|
145
121
|
const response = await callGraphAPI(
|
|
146
122
|
accessToken,
|
|
@@ -167,34 +143,15 @@ async function handleCreateRule(args) {
|
|
|
167
143
|
};
|
|
168
144
|
}
|
|
169
145
|
|
|
170
|
-
return
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
type: 'text',
|
|
174
|
-
text: "Failed to create rule. The server didn't return a rule ID.",
|
|
175
|
-
},
|
|
176
|
-
],
|
|
177
|
-
};
|
|
146
|
+
return toolError(
|
|
147
|
+
"Failed to create rule. The server didn't return a rule ID."
|
|
148
|
+
);
|
|
178
149
|
} catch (error) {
|
|
179
150
|
if (error.message === 'Authentication required') {
|
|
180
|
-
return
|
|
181
|
-
content: [
|
|
182
|
-
{
|
|
183
|
-
type: 'text',
|
|
184
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
185
|
-
},
|
|
186
|
-
],
|
|
187
|
-
};
|
|
151
|
+
return authRequiredError();
|
|
188
152
|
}
|
|
189
153
|
|
|
190
|
-
return {
|
|
191
|
-
content: [
|
|
192
|
-
{
|
|
193
|
-
type: 'text',
|
|
194
|
-
text: `Error creating rule: ${error.message}`,
|
|
195
|
-
},
|
|
196
|
-
],
|
|
197
|
-
};
|
|
154
|
+
return toolError(`Error creating rule: ${error.message}`);
|
|
198
155
|
}
|
|
199
156
|
}
|
|
200
157
|
|
package/rules/index.js
CHANGED
|
@@ -7,6 +7,8 @@ const handleUpdateRule = require('./update');
|
|
|
7
7
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
8
8
|
const { ensureAuthenticated } = require('../auth');
|
|
9
9
|
const { checkRateLimit } = require('../utils/safety');
|
|
10
|
+
const { toolMetadata } = require('../utils/risk-classes');
|
|
11
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Delete rule handler
|
|
@@ -16,19 +18,8 @@ const { checkRateLimit } = require('../utils/safety');
|
|
|
16
18
|
async function handleDeleteRule(args) {
|
|
17
19
|
const { ruleName, ruleId } = args;
|
|
18
20
|
|
|
19
|
-
// Rate limit
|
|
20
|
-
const rateLimitError = checkRateLimit('manage-rules');
|
|
21
|
-
if (rateLimitError) return rateLimitError;
|
|
22
|
-
|
|
23
21
|
if (!ruleName && !ruleId) {
|
|
24
|
-
return
|
|
25
|
-
content: [
|
|
26
|
-
{
|
|
27
|
-
type: 'text',
|
|
28
|
-
text: 'Either ruleName or ruleId is required.',
|
|
29
|
-
},
|
|
30
|
-
],
|
|
31
|
-
};
|
|
22
|
+
return toolError('Either ruleName or ruleId is required.');
|
|
32
23
|
}
|
|
33
24
|
|
|
34
25
|
try {
|
|
@@ -42,19 +33,17 @@ async function handleDeleteRule(args) {
|
|
|
42
33
|
const rules = await getInboxRules(accessToken);
|
|
43
34
|
const rule = rules.find((r) => r.displayName === ruleName);
|
|
44
35
|
if (!rule) {
|
|
45
|
-
return {
|
|
46
|
-
content: [
|
|
47
|
-
{
|
|
48
|
-
type: 'text',
|
|
49
|
-
text: `Rule with name "${ruleName}" not found.`,
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
};
|
|
36
|
+
return toolError(`Rule with name "${ruleName}" not found.`);
|
|
53
37
|
}
|
|
54
38
|
resolvedId = rule.id;
|
|
55
39
|
displayName = ruleName;
|
|
56
40
|
}
|
|
57
41
|
|
|
42
|
+
// Checked only once there is a rule to delete, so a bad call doesn't
|
|
43
|
+
// use up a slot (#279)
|
|
44
|
+
const rateLimitError = checkRateLimit('manage-rules');
|
|
45
|
+
if (rateLimitError) return rateLimitError;
|
|
46
|
+
|
|
58
47
|
await callGraphAPI(
|
|
59
48
|
accessToken,
|
|
60
49
|
'DELETE',
|
|
@@ -71,23 +60,9 @@ async function handleDeleteRule(args) {
|
|
|
71
60
|
};
|
|
72
61
|
} catch (error) {
|
|
73
62
|
if (error.message === 'Authentication required') {
|
|
74
|
-
return
|
|
75
|
-
content: [
|
|
76
|
-
{
|
|
77
|
-
type: 'text',
|
|
78
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
79
|
-
},
|
|
80
|
-
],
|
|
81
|
-
};
|
|
63
|
+
return authRequiredError();
|
|
82
64
|
}
|
|
83
|
-
return {
|
|
84
|
-
content: [
|
|
85
|
-
{
|
|
86
|
-
type: 'text',
|
|
87
|
-
text: `Error deleting rule: ${error.message}`,
|
|
88
|
-
},
|
|
89
|
-
],
|
|
90
|
-
};
|
|
65
|
+
return toolError(`Error deleting rule: ${error.message}`);
|
|
91
66
|
}
|
|
92
67
|
}
|
|
93
68
|
|
|
@@ -100,25 +75,15 @@ async function handleEditRuleSequence(args) {
|
|
|
100
75
|
const { ruleName, sequence } = args;
|
|
101
76
|
|
|
102
77
|
if (!ruleName) {
|
|
103
|
-
return
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
type: 'text',
|
|
107
|
-
text: 'Rule name is required. Please specify the exact name of an existing rule.',
|
|
108
|
-
},
|
|
109
|
-
],
|
|
110
|
-
};
|
|
78
|
+
return toolError(
|
|
79
|
+
'Rule name is required. Please specify the exact name of an existing rule.'
|
|
80
|
+
);
|
|
111
81
|
}
|
|
112
82
|
|
|
113
83
|
if (!sequence || isNaN(sequence) || sequence < 1) {
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
type: 'text',
|
|
118
|
-
text: 'A positive sequence number is required. Lower numbers run first (higher priority).',
|
|
119
|
-
},
|
|
120
|
-
],
|
|
121
|
-
};
|
|
84
|
+
return toolError(
|
|
85
|
+
'A positive sequence number is required. Lower numbers run first (higher priority).'
|
|
86
|
+
);
|
|
122
87
|
}
|
|
123
88
|
|
|
124
89
|
try {
|
|
@@ -127,16 +92,13 @@ async function handleEditRuleSequence(args) {
|
|
|
127
92
|
|
|
128
93
|
const rule = rules.find((r) => r.displayName === ruleName);
|
|
129
94
|
if (!rule) {
|
|
130
|
-
return {
|
|
131
|
-
content: [
|
|
132
|
-
{
|
|
133
|
-
type: 'text',
|
|
134
|
-
text: `Rule with name "${ruleName}" not found.`,
|
|
135
|
-
},
|
|
136
|
-
],
|
|
137
|
-
};
|
|
95
|
+
return toolError(`Rule with name "${ruleName}" not found.`);
|
|
138
96
|
}
|
|
139
97
|
|
|
98
|
+
// Reordering is a write, so it shares the manage-rules limit (#273)
|
|
99
|
+
const rateLimitError = checkRateLimit('manage-rules');
|
|
100
|
+
if (rateLimitError) return rateLimitError;
|
|
101
|
+
|
|
140
102
|
await callGraphAPI(
|
|
141
103
|
accessToken,
|
|
142
104
|
'PATCH',
|
|
@@ -154,24 +116,10 @@ async function handleEditRuleSequence(args) {
|
|
|
154
116
|
};
|
|
155
117
|
} catch (error) {
|
|
156
118
|
if (error.message === 'Authentication required') {
|
|
157
|
-
return
|
|
158
|
-
content: [
|
|
159
|
-
{
|
|
160
|
-
type: 'text',
|
|
161
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
162
|
-
},
|
|
163
|
-
],
|
|
164
|
-
};
|
|
119
|
+
return authRequiredError();
|
|
165
120
|
}
|
|
166
121
|
|
|
167
|
-
return {
|
|
168
|
-
content: [
|
|
169
|
-
{
|
|
170
|
-
type: 'text',
|
|
171
|
-
text: `Error updating rule sequence: ${error.message}`,
|
|
172
|
-
},
|
|
173
|
-
],
|
|
174
|
-
};
|
|
122
|
+
return toolError(`Error updating rule sequence: ${error.message}`);
|
|
175
123
|
}
|
|
176
124
|
}
|
|
177
125
|
|
|
@@ -180,13 +128,8 @@ const rulesTools = [
|
|
|
180
128
|
{
|
|
181
129
|
name: 'manage-rules',
|
|
182
130
|
description:
|
|
183
|
-
'Server-side inbox
|
|
184
|
-
|
|
185
|
-
title: 'Inbox Rules',
|
|
186
|
-
readOnlyHint: false,
|
|
187
|
-
destructiveHint: true,
|
|
188
|
-
openWorldHint: false,
|
|
189
|
-
},
|
|
131
|
+
'Server-side inbox rules (destructive: covers `delete`; `dryRun` previews create/update). Rules run on the Exchange server whichever client is open. action=`list` (default) returns rules with id/name/sequence; `includeDetails: true` adds conditions/actions/exceptions. action=`create` builds a rule from condition params (12, e.g. fromAddresses, containsSubject, bodyContains, hasAttachments, importance, sentTo, sensitivity), action params (9, e.g. moveToFolder/copyToFolder — folder name, nested path like `Triage/Delete`, or ID — forwardTo, redirectTo, assignCategories, markAsRead, delete) and optional `except*` exceptions. action=`update` patches fields by `ruleId` or `ruleName`. action=`reorder` changes priority via `sequence` (lower runs first). action=`delete` removes a rule. The recipient allowlist applies to forwardTo/redirectTo. There is deliberately no `permanentDelete` action (too dangerous for AI use; use the Outlook UI). Subject to session rate limits (`OUTLOOK_MAX_MANAGE_RULES_PER_SESSION`).',
|
|
132
|
+
...toolMetadata('manage-rules', 'Inbox Rules'),
|
|
190
133
|
inputSchema: {
|
|
191
134
|
type: 'object',
|
|
192
135
|
properties: {
|
|
@@ -217,7 +160,7 @@ const rulesTools = [
|
|
|
217
160
|
dryRun: {
|
|
218
161
|
type: 'boolean',
|
|
219
162
|
description:
|
|
220
|
-
'Preview
|
|
163
|
+
'Preview only (action=create or update): shows the rule without creating or changing it. Other actions refuse dryRun and change nothing. Default false.',
|
|
221
164
|
},
|
|
222
165
|
isEnabled: {
|
|
223
166
|
type: 'boolean',
|
|
@@ -404,14 +347,9 @@ const rulesTools = [
|
|
|
404
347
|
case 'list':
|
|
405
348
|
return handleListRules(args);
|
|
406
349
|
default:
|
|
407
|
-
return
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
type: 'text',
|
|
411
|
-
text: `Unknown action '${action}'. Valid actions: list, create, update, reorder, delete.`,
|
|
412
|
-
},
|
|
413
|
-
],
|
|
414
|
-
};
|
|
350
|
+
return toolError(
|
|
351
|
+
`Unknown action '${action}'. Valid actions: list, create, update, reorder, delete.`
|
|
352
|
+
);
|
|
415
353
|
}
|
|
416
354
|
},
|
|
417
355
|
},
|
package/rules/list.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
5
5
|
const { ensureAuthenticated } = require('../auth');
|
|
6
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
7
|
+
const { log } = require('../utils/logger');
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* List rules handler
|
|
@@ -27,24 +29,10 @@ async function handleListRules(args) {
|
|
|
27
29
|
};
|
|
28
30
|
} catch (error) {
|
|
29
31
|
if (error.message === 'Authentication required') {
|
|
30
|
-
return
|
|
31
|
-
content: [
|
|
32
|
-
{
|
|
33
|
-
type: 'text',
|
|
34
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
35
|
-
},
|
|
36
|
-
],
|
|
37
|
-
};
|
|
32
|
+
return authRequiredError();
|
|
38
33
|
}
|
|
39
34
|
|
|
40
|
-
return {
|
|
41
|
-
content: [
|
|
42
|
-
{
|
|
43
|
-
type: 'text',
|
|
44
|
-
text: `Error listing rules: ${error.message}`,
|
|
45
|
-
},
|
|
46
|
-
],
|
|
47
|
-
};
|
|
35
|
+
return toolError(`Error listing rules: ${error.message}`);
|
|
48
36
|
}
|
|
49
37
|
}
|
|
50
38
|
|
|
@@ -64,7 +52,7 @@ async function getInboxRules(accessToken) {
|
|
|
64
52
|
|
|
65
53
|
return response.value || [];
|
|
66
54
|
} catch (error) {
|
|
67
|
-
|
|
55
|
+
log.debug(`Error getting inbox rules: ${error.message}`);
|
|
68
56
|
throw error;
|
|
69
57
|
}
|
|
70
58
|
}
|
package/rules/rule-builder.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
const { resolveFolder, looksLikeFolderId } = require('../folder/resolve');
|
|
6
6
|
const { checkRecipientAllowlist } = require('../utils/safety');
|
|
7
|
+
const { toolError } = require('../utils/tool-error');
|
|
7
8
|
|
|
8
9
|
const VALID_IMPORTANCE = ['low', 'normal', 'high'];
|
|
9
10
|
const VALID_SENSITIVITY = ['normal', 'personal', 'private', 'confidential'];
|
|
@@ -197,38 +198,35 @@ async function buildActions(args, accessToken) {
|
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
200
|
|
|
200
|
-
// Recipient-based actions (comma-separated emails → recipient objects)
|
|
201
|
+
// Recipient-based actions (comma-separated emails → recipient objects).
|
|
202
|
+
// create/update refuse blocked recipients up front (checkRuleRecipients);
|
|
203
|
+
// this is the backstop, so a blocked forward is never silently dropped.
|
|
201
204
|
if (args.forwardTo) {
|
|
202
205
|
const recipients = formatRecipientObjects(args.forwardTo);
|
|
203
206
|
if (recipients.length > 0) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
warnings.push(
|
|
208
|
-
`Forward recipients blocked by allowlist: ${args.forwardTo}`
|
|
209
|
-
);
|
|
210
|
-
} else {
|
|
211
|
-
actions.forwardTo = recipients;
|
|
212
|
-
warnings.push(
|
|
213
|
-
`This rule will forward all matching emails to: ${args.forwardTo}. Verify these addresses are correct.`
|
|
207
|
+
if (checkRecipientAllowlist(recipients)) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`forwardTo recipients blocked by OUTLOOK_ALLOWED_RECIPIENTS: ${args.forwardTo}`
|
|
214
210
|
);
|
|
215
211
|
}
|
|
212
|
+
actions.forwardTo = recipients;
|
|
213
|
+
warnings.push(
|
|
214
|
+
`This rule will forward all matching emails to: ${args.forwardTo}. Verify these addresses are correct.`
|
|
215
|
+
);
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
if (args.redirectTo) {
|
|
219
219
|
const recipients = formatRecipientObjects(args.redirectTo);
|
|
220
220
|
if (recipients.length > 0) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
`Redirect recipients blocked by allowlist: ${args.redirectTo}`
|
|
225
|
-
);
|
|
226
|
-
} else {
|
|
227
|
-
actions.redirectTo = recipients;
|
|
228
|
-
warnings.push(
|
|
229
|
-
`This rule will redirect all matching emails to: ${args.redirectTo}. The original sender will appear as the sender.`
|
|
221
|
+
if (checkRecipientAllowlist(recipients)) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`redirectTo recipients blocked by OUTLOOK_ALLOWED_RECIPIENTS: ${args.redirectTo}`
|
|
230
224
|
);
|
|
231
225
|
}
|
|
226
|
+
actions.redirectTo = recipients;
|
|
227
|
+
warnings.push(
|
|
228
|
+
`This rule will redirect all matching emails to: ${args.redirectTo}. The original sender will appear as the sender.`
|
|
229
|
+
);
|
|
232
230
|
}
|
|
233
231
|
}
|
|
234
232
|
|
|
@@ -240,6 +238,44 @@ async function buildActions(args, accessToken) {
|
|
|
240
238
|
return { actions, warnings };
|
|
241
239
|
}
|
|
242
240
|
|
|
241
|
+
/** Rule actions that send matching mail to other people. */
|
|
242
|
+
const RECIPIENT_ACTIONS = ['forwardTo', 'redirectTo'];
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Refuse a rule whose forwardTo/redirectTo includes an address that
|
|
246
|
+
* OUTLOOK_ALLOWED_RECIPIENTS blocks. The whole rule (or update) is refused,
|
|
247
|
+
* never saved with the blocked forwarding dropped, and a dry run reports the
|
|
248
|
+
* refusal too. Needs no Graph call, so it runs before sign-in. (#273)
|
|
249
|
+
* @param {object} args - Tool arguments
|
|
250
|
+
* @param {{operation?: 'create'|'update', dryRun?: boolean}} [options]
|
|
251
|
+
* @returns {object|null} - toolError naming the blocked addresses, or null
|
|
252
|
+
*/
|
|
253
|
+
function checkRuleRecipients(args, { operation = 'create', dryRun } = {}) {
|
|
254
|
+
const blocked = [];
|
|
255
|
+
for (const param of RECIPIENT_ACTIONS) {
|
|
256
|
+
const addresses = formatRecipientObjects(args[param])
|
|
257
|
+
.filter((recipient) => checkRecipientAllowlist([recipient]))
|
|
258
|
+
.map((recipient) => recipient.emailAddress.address);
|
|
259
|
+
if (addresses.length > 0) {
|
|
260
|
+
blocked.push(`${param}: ${addresses.join(', ')}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (blocked.length === 0) return null;
|
|
264
|
+
|
|
265
|
+
const subject = operation === 'update' ? 'Rule update' : 'Rule';
|
|
266
|
+
const lead = dryRun
|
|
267
|
+
? `DRY RUN — ${subject.toLowerCase()} would be refused`
|
|
268
|
+
: `${subject} refused`;
|
|
269
|
+
const outcome = operation === 'update' ? 'changed' : 'created';
|
|
270
|
+
return toolError(
|
|
271
|
+
`${lead}: OUTLOOK_ALLOWED_RECIPIENTS does not allow ${blocked.join('; ')}. Nothing was ${outcome}; a rule is refused whole rather than saved without the blocked forwarding.`,
|
|
272
|
+
{
|
|
273
|
+
nextStep:
|
|
274
|
+
'Remove the blocked addresses from forwardTo/redirectTo and retry, or ask the user to add them to OUTLOOK_ALLOWED_RECIPIENTS in the server configuration and restart the server.',
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
243
279
|
/**
|
|
244
280
|
* Build a Graph API exceptions object from except* prefixed parameters.
|
|
245
281
|
* @param {object} args - Tool arguments
|
|
@@ -324,6 +360,7 @@ module.exports = {
|
|
|
324
360
|
buildConditions,
|
|
325
361
|
buildActions,
|
|
326
362
|
buildExceptions,
|
|
363
|
+
checkRuleRecipients,
|
|
327
364
|
hasAnyCondition,
|
|
328
365
|
hasAnyAction,
|
|
329
366
|
};
|
package/rules/update.js
CHANGED
|
@@ -9,7 +9,9 @@ const {
|
|
|
9
9
|
buildConditions,
|
|
10
10
|
buildActions,
|
|
11
11
|
buildExceptions,
|
|
12
|
+
checkRuleRecipients,
|
|
12
13
|
} = require('./rule-builder');
|
|
14
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Update rule handler — modify an existing rule by name or ID.
|
|
@@ -20,32 +22,23 @@ const {
|
|
|
20
22
|
async function handleUpdateRule(args) {
|
|
21
23
|
const { ruleName, ruleId, name, isEnabled, sequence, dryRun } = args;
|
|
22
24
|
|
|
23
|
-
// Rate limit
|
|
24
|
-
const rateLimitError = checkRateLimit('manage-rules');
|
|
25
|
-
if (rateLimitError) return rateLimitError;
|
|
26
|
-
|
|
27
25
|
if (!ruleName && !ruleId) {
|
|
28
|
-
return
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
type: 'text',
|
|
32
|
-
text: 'Either ruleName or ruleId is required to identify the rule to update.',
|
|
33
|
-
},
|
|
34
|
-
],
|
|
35
|
-
};
|
|
26
|
+
return toolError(
|
|
27
|
+
'Either ruleName or ruleId is required to identify the rule to update.'
|
|
28
|
+
);
|
|
36
29
|
}
|
|
37
30
|
|
|
38
31
|
if (sequence !== undefined && (isNaN(sequence) || sequence < 1)) {
|
|
39
|
-
return
|
|
40
|
-
content: [
|
|
41
|
-
{
|
|
42
|
-
type: 'text',
|
|
43
|
-
text: 'Sequence must be a positive number greater than zero.',
|
|
44
|
-
},
|
|
45
|
-
],
|
|
46
|
-
};
|
|
32
|
+
return toolError('Sequence must be a positive number greater than zero.');
|
|
47
33
|
}
|
|
48
34
|
|
|
35
|
+
// Refuse the whole update if the allowlist blocks any forwarding (#273)
|
|
36
|
+
const recipientError = checkRuleRecipients(args, {
|
|
37
|
+
operation: 'update',
|
|
38
|
+
dryRun,
|
|
39
|
+
});
|
|
40
|
+
if (recipientError) return recipientError;
|
|
41
|
+
|
|
49
42
|
try {
|
|
50
43
|
const accessToken = await ensureAuthenticated();
|
|
51
44
|
|
|
@@ -63,14 +56,7 @@ async function handleUpdateRule(args) {
|
|
|
63
56
|
}
|
|
64
57
|
|
|
65
58
|
if (!currentRule) {
|
|
66
|
-
return {
|
|
67
|
-
content: [
|
|
68
|
-
{
|
|
69
|
-
type: 'text',
|
|
70
|
-
text: `Rule "${ruleName || ruleId}" not found.`,
|
|
71
|
-
},
|
|
72
|
-
],
|
|
73
|
-
};
|
|
59
|
+
return toolError(`Rule "${ruleName || ruleId}" not found.`);
|
|
74
60
|
}
|
|
75
61
|
|
|
76
62
|
// Build PATCH payload — only include fields the user provided
|
|
@@ -107,14 +93,9 @@ async function handleUpdateRule(args) {
|
|
|
107
93
|
// Check for fatal folder-not-found
|
|
108
94
|
const folderNotFound = actWarnings.some((w) => w.includes('not found'));
|
|
109
95
|
if (folderNotFound && Object.keys(actions).length === 0) {
|
|
110
|
-
return
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
type: 'text',
|
|
114
|
-
text: actWarnings.filter((w) => w.includes('not found')).join('\n'),
|
|
115
|
-
},
|
|
116
|
-
],
|
|
117
|
-
};
|
|
96
|
+
return toolError(
|
|
97
|
+
actWarnings.filter((w) => w.includes('not found')).join('\n')
|
|
98
|
+
);
|
|
118
99
|
}
|
|
119
100
|
|
|
120
101
|
// Build exceptions if any except* param was provided
|
|
@@ -126,14 +107,9 @@ async function handleUpdateRule(args) {
|
|
|
126
107
|
|
|
127
108
|
// Check that something is actually being changed
|
|
128
109
|
if (Object.keys(patch).length === 0) {
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
type: 'text',
|
|
133
|
-
text: `No changes specified for rule "${currentRule.displayName}". Provide at least one parameter to update (name, isEnabled, sequence, conditions, actions, or exceptions).`,
|
|
134
|
-
},
|
|
135
|
-
],
|
|
136
|
-
};
|
|
110
|
+
return toolError(
|
|
111
|
+
`No changes specified for rule "${currentRule.displayName}". Provide at least one parameter to update (name, isEnabled, sequence, conditions, actions, or exceptions).`
|
|
112
|
+
);
|
|
137
113
|
}
|
|
138
114
|
|
|
139
115
|
// Dry-run: show before/after preview
|
|
@@ -153,6 +129,10 @@ async function handleUpdateRule(args) {
|
|
|
153
129
|
};
|
|
154
130
|
}
|
|
155
131
|
|
|
132
|
+
// Rate limit only real writes, so a dry run never uses up a slot (#273)
|
|
133
|
+
const rateLimitError = checkRateLimit('manage-rules');
|
|
134
|
+
if (rateLimitError) return rateLimitError;
|
|
135
|
+
|
|
156
136
|
// Execute PATCH
|
|
157
137
|
await callGraphAPI(
|
|
158
138
|
accessToken,
|
|
@@ -197,24 +177,10 @@ async function handleUpdateRule(args) {
|
|
|
197
177
|
};
|
|
198
178
|
} catch (error) {
|
|
199
179
|
if (error.message === 'Authentication required') {
|
|
200
|
-
return
|
|
201
|
-
content: [
|
|
202
|
-
{
|
|
203
|
-
type: 'text',
|
|
204
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
205
|
-
},
|
|
206
|
-
],
|
|
207
|
-
};
|
|
180
|
+
return authRequiredError();
|
|
208
181
|
}
|
|
209
182
|
|
|
210
|
-
return {
|
|
211
|
-
content: [
|
|
212
|
-
{
|
|
213
|
-
type: 'text',
|
|
214
|
-
text: `Error updating rule: ${error.message}`,
|
|
215
|
-
},
|
|
216
|
-
],
|
|
217
|
-
};
|
|
183
|
+
return toolError(`Error updating rule: ${error.message}`);
|
|
218
184
|
}
|
|
219
185
|
}
|
|
220
186
|
|