@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/README.md +9 -6
- package/advanced/index.js +5 -5
- package/auth/device-code.js +3 -1
- package/auth/oauth-server.js +4 -4
- package/auth/token-manager.js +2 -2
- package/calendar/list.js +3 -3
- package/categories/index.js +4 -4
- package/contacts/index.js +11 -18
- package/email/conversations.js +19 -17
- package/email/delta.js +1 -1
- package/email/draft.js +482 -0
- package/email/folder-utils.js +1 -1
- package/email/headers.js +10 -10
- package/email/index.js +80 -0
- package/email/list.js +1 -1
- package/email/mime.js +13 -12
- package/email/search.js +3 -2
- package/email/send.js +1 -1
- package/folder/list.js +1 -1
- package/llms.txt +8 -7
- package/package.json +2 -2
- package/rules/create.js +85 -161
- package/rules/index.js +162 -29
- package/rules/list.js +144 -62
- package/rules/rule-builder.js +284 -0
- package/rules/update.js +213 -0
- package/settings/index.js +10 -22
- package/utils/field-presets.js +20 -0
- package/utils/graph-api.js +9 -1
- package/utils/response-formatter.js +8 -6
- package/utils/safety.js +113 -0
|
@@ -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
|
+
};
|
package/rules/update.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update rule functionality
|
|
3
|
+
*/
|
|
4
|
+
const { callGraphAPI } = require('../utils/graph-api');
|
|
5
|
+
const { ensureAuthenticated } = require('../auth');
|
|
6
|
+
const { checkRateLimit, formatRuleDryRunPreview } = require('../utils/safety');
|
|
7
|
+
const { getInboxRules } = require('./list');
|
|
8
|
+
const {
|
|
9
|
+
buildConditions,
|
|
10
|
+
buildActions,
|
|
11
|
+
buildExceptions,
|
|
12
|
+
} = require('./rule-builder');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Update rule handler — modify an existing rule by name or ID.
|
|
16
|
+
* Conditions, actions, and exceptions use replace semantics (not merge).
|
|
17
|
+
* @param {object} args - Tool arguments
|
|
18
|
+
* @returns {object} - MCP response
|
|
19
|
+
*/
|
|
20
|
+
async function handleUpdateRule(args) {
|
|
21
|
+
const { ruleName, ruleId, name, isEnabled, sequence, dryRun } = args;
|
|
22
|
+
|
|
23
|
+
// Rate limit
|
|
24
|
+
const rateLimitError = checkRateLimit('manage-rules');
|
|
25
|
+
if (rateLimitError) return rateLimitError;
|
|
26
|
+
|
|
27
|
+
if (!ruleName && !ruleId) {
|
|
28
|
+
return {
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
type: 'text',
|
|
32
|
+
text: 'Either ruleName or ruleId is required to identify the rule to update.',
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
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
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const accessToken = await ensureAuthenticated();
|
|
51
|
+
|
|
52
|
+
// Resolve rule ID
|
|
53
|
+
let resolvedId = ruleId;
|
|
54
|
+
let currentRule = null;
|
|
55
|
+
|
|
56
|
+
const rules = await getInboxRules(accessToken);
|
|
57
|
+
|
|
58
|
+
if (resolvedId) {
|
|
59
|
+
currentRule = rules.find((r) => r.id === resolvedId);
|
|
60
|
+
} else {
|
|
61
|
+
currentRule = rules.find((r) => r.displayName === ruleName);
|
|
62
|
+
if (currentRule) resolvedId = currentRule.id;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!currentRule) {
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: 'text',
|
|
70
|
+
text: `Rule "${ruleName || ruleId}" not found.`,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Build PATCH payload — only include fields the user provided
|
|
77
|
+
const patch = {};
|
|
78
|
+
const allWarnings = [];
|
|
79
|
+
|
|
80
|
+
if (name) patch.displayName = name;
|
|
81
|
+
if (isEnabled !== undefined) patch.isEnabled = isEnabled;
|
|
82
|
+
if (sequence) patch.sequence = Math.max(1, Math.floor(sequence));
|
|
83
|
+
|
|
84
|
+
// Build conditions if any condition param was provided
|
|
85
|
+
const { conditions, warnings: condWarnings } = buildConditions(args);
|
|
86
|
+
if (Object.keys(conditions).length > 0) {
|
|
87
|
+
patch.conditions = conditions;
|
|
88
|
+
allWarnings.push(
|
|
89
|
+
'Conditions replaced (not merged). Use dryRun=true to preview before updating.'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
allWarnings.push(...condWarnings);
|
|
93
|
+
|
|
94
|
+
// Build actions if any action param was provided
|
|
95
|
+
const { actions, warnings: actWarnings } = await buildActions(
|
|
96
|
+
args,
|
|
97
|
+
accessToken
|
|
98
|
+
);
|
|
99
|
+
if (Object.keys(actions).length > 0) {
|
|
100
|
+
patch.actions = actions;
|
|
101
|
+
allWarnings.push(
|
|
102
|
+
'Actions replaced (not merged). Use dryRun=true to preview before updating.'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
allWarnings.push(...actWarnings);
|
|
106
|
+
|
|
107
|
+
// Check for fatal folder-not-found
|
|
108
|
+
const folderNotFound = actWarnings.some((w) => w.includes('not found'));
|
|
109
|
+
if (folderNotFound && Object.keys(actions).length === 0) {
|
|
110
|
+
return {
|
|
111
|
+
content: [
|
|
112
|
+
{
|
|
113
|
+
type: 'text',
|
|
114
|
+
text: actWarnings.filter((w) => w.includes('not found')).join('\n'),
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Build exceptions if any except* param was provided
|
|
121
|
+
const { exceptions, warnings: excWarnings } = buildExceptions(args);
|
|
122
|
+
if (Object.keys(exceptions).length > 0) {
|
|
123
|
+
patch.exceptions = exceptions;
|
|
124
|
+
}
|
|
125
|
+
allWarnings.push(...excWarnings);
|
|
126
|
+
|
|
127
|
+
// Check that something is actually being changed
|
|
128
|
+
if (Object.keys(patch).length === 0) {
|
|
129
|
+
return {
|
|
130
|
+
content: [
|
|
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
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Dry-run: show before/after preview
|
|
140
|
+
if (dryRun) {
|
|
141
|
+
const updatedRule = { ...currentRule, ...patch };
|
|
142
|
+
const currentPreview = formatRuleDryRunPreview(currentRule);
|
|
143
|
+
const updatedPreview = formatRuleDryRunPreview(updatedRule);
|
|
144
|
+
|
|
145
|
+
let text = `DRY RUN — Update preview for "${currentRule.displayName}" (not applied):\n\n`;
|
|
146
|
+
text += `CURRENT:\n${currentPreview}\n\n`;
|
|
147
|
+
text += `AFTER UPDATE:\n${updatedPreview}`;
|
|
148
|
+
if (allWarnings.length > 0) {
|
|
149
|
+
text += `\n\nWarnings:\n${allWarnings.map((w) => `- ${w}`).join('\n')}`;
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
content: [{ type: 'text', text }],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Execute PATCH
|
|
157
|
+
await callGraphAPI(
|
|
158
|
+
accessToken,
|
|
159
|
+
'PATCH',
|
|
160
|
+
`me/mailFolders/inbox/messageRules/${resolvedId}`,
|
|
161
|
+
patch
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const changedFields = Object.keys(patch)
|
|
165
|
+
.map((k) => {
|
|
166
|
+
if (k === 'displayName') return `name → "${patch.displayName}"`;
|
|
167
|
+
if (k === 'isEnabled') {
|
|
168
|
+
return patch.isEnabled ? 'enabled' : 'disabled';
|
|
169
|
+
}
|
|
170
|
+
if (k === 'sequence') return `sequence → ${patch.sequence}`;
|
|
171
|
+
if (k === 'conditions') return 'conditions updated';
|
|
172
|
+
if (k === 'actions') return 'actions updated';
|
|
173
|
+
if (k === 'exceptions') return 'exceptions updated';
|
|
174
|
+
return k;
|
|
175
|
+
})
|
|
176
|
+
.join(', ');
|
|
177
|
+
|
|
178
|
+
let text = `Successfully updated rule "${currentRule.displayName}": ${changedFields}.`;
|
|
179
|
+
// Filter out the replace/merge info warnings from the response since the update is done
|
|
180
|
+
const relevantWarnings = allWarnings.filter(
|
|
181
|
+
(w) => !w.includes('replaced (not merged)')
|
|
182
|
+
);
|
|
183
|
+
if (relevantWarnings.length > 0) {
|
|
184
|
+
text += `\n\nNotes:\n${relevantWarnings.map((w) => `- ${w}`).join('\n')}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
content: [{ type: 'text', text }],
|
|
189
|
+
};
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (error.message === 'Authentication required') {
|
|
192
|
+
return {
|
|
193
|
+
content: [
|
|
194
|
+
{
|
|
195
|
+
type: 'text',
|
|
196
|
+
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
content: [
|
|
204
|
+
{
|
|
205
|
+
type: 'text',
|
|
206
|
+
text: `Error updating rule: ${error.message}`,
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = handleUpdateRule;
|
package/settings/index.js
CHANGED
|
@@ -56,13 +56,7 @@ function formatAutomaticReplies(settings) {
|
|
|
56
56
|
|
|
57
57
|
const lines = [];
|
|
58
58
|
lines.push(
|
|
59
|
-
`**Status**: ${
|
|
60
|
-
settings.status === 'disabled'
|
|
61
|
-
? '🔴 Disabled'
|
|
62
|
-
: settings.status === 'alwaysEnabled'
|
|
63
|
-
? '🟢 Always Enabled'
|
|
64
|
-
: '📅 Scheduled'
|
|
65
|
-
}`
|
|
59
|
+
`**Status**: ${{ disabled: '🔴 Disabled', alwaysEnabled: '🟢 Always Enabled' }[settings.status] || '📅 Scheduled'}`
|
|
66
60
|
);
|
|
67
61
|
|
|
68
62
|
if (settings.status !== 'disabled') {
|
|
@@ -113,7 +107,7 @@ async function handleGetMailboxSettings(args) {
|
|
|
113
107
|
|
|
114
108
|
const settings = await callGraphAPI(accessToken, 'GET', endpoint);
|
|
115
109
|
|
|
116
|
-
|
|
110
|
+
const output = [];
|
|
117
111
|
output.push('# Mailbox Settings\n');
|
|
118
112
|
|
|
119
113
|
if (section && section !== 'all') {
|
|
@@ -287,7 +281,7 @@ async function handleSetAutomaticReplies(args) {
|
|
|
287
281
|
'me/mailboxSettings/automaticRepliesSetting'
|
|
288
282
|
);
|
|
289
283
|
|
|
290
|
-
|
|
284
|
+
const output = [];
|
|
291
285
|
output.push('Automatic replies updated!\n');
|
|
292
286
|
output.push(formatAutomaticReplies(updated));
|
|
293
287
|
|
|
@@ -413,17 +407,11 @@ async function handleSetWorkingHours(args) {
|
|
|
413
407
|
);
|
|
414
408
|
|
|
415
409
|
// Build update
|
|
410
|
+
const padTime = (t) =>
|
|
411
|
+
t.length === 5 ? `${t}:00.0000000` : `${t}.0000000`;
|
|
416
412
|
const workingHours = {
|
|
417
|
-
startTime: startTime
|
|
418
|
-
|
|
419
|
-
? startTime + ':00.0000000'
|
|
420
|
-
: startTime + '.0000000'
|
|
421
|
-
: current.startTime,
|
|
422
|
-
endTime: endTime
|
|
423
|
-
? endTime.length === 5
|
|
424
|
-
? endTime + ':00.0000000'
|
|
425
|
-
: endTime + '.0000000'
|
|
426
|
-
: current.endTime,
|
|
413
|
+
startTime: startTime ? padTime(startTime) : current.startTime,
|
|
414
|
+
endTime: endTime ? padTime(endTime) : current.endTime,
|
|
427
415
|
daysOfWeek: daysOfWeek
|
|
428
416
|
? daysOfWeek.map((d) => d.toLowerCase())
|
|
429
417
|
: current.daysOfWeek,
|
|
@@ -442,7 +430,7 @@ async function handleSetWorkingHours(args) {
|
|
|
442
430
|
'me/mailboxSettings/workingHours'
|
|
443
431
|
);
|
|
444
432
|
|
|
445
|
-
|
|
433
|
+
const output = [];
|
|
446
434
|
output.push('Working hours updated!\n');
|
|
447
435
|
output.push(formatWorkingHours(updated));
|
|
448
436
|
|
|
@@ -491,7 +479,7 @@ async function handleGetAutomaticReplies() {
|
|
|
491
479
|
'me/mailboxSettings/automaticRepliesSetting'
|
|
492
480
|
);
|
|
493
481
|
|
|
494
|
-
|
|
482
|
+
const output = [];
|
|
495
483
|
output.push('# Automatic Replies\n');
|
|
496
484
|
output.push(formatAutomaticReplies(settings));
|
|
497
485
|
|
|
@@ -533,7 +521,7 @@ async function handleGetWorkingHours() {
|
|
|
533
521
|
'me/mailboxSettings/workingHours'
|
|
534
522
|
);
|
|
535
523
|
|
|
536
|
-
|
|
524
|
+
const output = [];
|
|
537
525
|
output.push('# Working Hours\n');
|
|
538
526
|
output.push(formatWorkingHours(settings));
|
|
539
527
|
|
package/utils/field-presets.js
CHANGED
|
@@ -107,6 +107,26 @@ const FIELD_PRESETS = {
|
|
|
107
107
|
'changeKey',
|
|
108
108
|
],
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Draft fields for viewing/editing drafts
|
|
112
|
+
* Use case: Draft creation, update, listing
|
|
113
|
+
*/
|
|
114
|
+
draft: [
|
|
115
|
+
'id',
|
|
116
|
+
'subject',
|
|
117
|
+
'from',
|
|
118
|
+
'toRecipients',
|
|
119
|
+
'ccRecipients',
|
|
120
|
+
'bccRecipients',
|
|
121
|
+
'body',
|
|
122
|
+
'bodyPreview',
|
|
123
|
+
'lastModifiedDateTime',
|
|
124
|
+
'isDraft',
|
|
125
|
+
'importance',
|
|
126
|
+
'hasAttachments',
|
|
127
|
+
'conversationId',
|
|
128
|
+
],
|
|
129
|
+
|
|
110
130
|
/**
|
|
111
131
|
* Search result fields (optimized for relevance display)
|
|
112
132
|
* Use case: Search results with context
|
package/utils/graph-api.js
CHANGED
|
@@ -7,6 +7,7 @@ const mockData = require('./mock-data');
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Makes a request to the Microsoft Graph API
|
|
10
|
+
* In test mode (USE_TEST_MODE=true), routes to mock data instead of the real API.
|
|
10
11
|
* @param {string} accessToken - The access token for authentication
|
|
11
12
|
* @param {string} method - HTTP method (GET, POST, etc.)
|
|
12
13
|
* @param {string} path - API endpoint path
|
|
@@ -14,6 +15,8 @@ const mockData = require('./mock-data');
|
|
|
14
15
|
* @param {object} queryParams - Query parameters
|
|
15
16
|
* @param {object} extraHeaders - Additional headers (e.g. Prefer for immutable IDs)
|
|
16
17
|
* @returns {Promise<object>} - The API response
|
|
18
|
+
* @throws {Error} 'UNAUTHORIZED' if the server returns HTTP 401 (token expired or invalid)
|
|
19
|
+
* @throws {Error} If the HTTP status is outside 2xx, or if JSON parsing or network fails
|
|
17
20
|
*/
|
|
18
21
|
async function callGraphAPI(
|
|
19
22
|
accessToken,
|
|
@@ -69,7 +72,7 @@ async function callGraphAPI(
|
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
if (queryString) {
|
|
72
|
-
queryString =
|
|
75
|
+
queryString = `?${queryString}`;
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
|
|
@@ -153,6 +156,8 @@ async function callGraphAPI(
|
|
|
153
156
|
* @param {object} queryParams - Initial query parameters
|
|
154
157
|
* @param {number} maxCount - Maximum number of items to retrieve (0 = all)
|
|
155
158
|
* @returns {Promise<object>} - Combined API response with all items
|
|
159
|
+
* @throws {Error} If method is not 'GET'
|
|
160
|
+
* @throws {Error} If any page request fails for any other reason
|
|
156
161
|
*/
|
|
157
162
|
async function callGraphAPIPaginated(
|
|
158
163
|
accessToken,
|
|
@@ -268,9 +273,12 @@ async function callGraphAPIBatch(accessToken, requests) {
|
|
|
268
273
|
|
|
269
274
|
/**
|
|
270
275
|
* Calls Graph API to get raw MIME content (for email export)
|
|
276
|
+
* In test mode (USE_TEST_MODE=true), returns mock MIME content instead of calling the real API.
|
|
271
277
|
* @param {string} accessToken - The access token for authentication
|
|
272
278
|
* @param {string} emailId - The email ID to export
|
|
273
279
|
* @returns {Promise<string>} - Raw MIME content as string
|
|
280
|
+
* @throws {Error} 'UNAUTHORIZED' if the server returns HTTP 401 (token expired or invalid)
|
|
281
|
+
* @throws {Error} If the HTTP status is outside 2xx or a network error occurs
|
|
274
282
|
*/
|
|
275
283
|
async function callGraphAPIRaw(accessToken, emailId) {
|
|
276
284
|
// Test mode: return mock MIME content
|
|
@@ -187,7 +187,7 @@ function formatEmailListAsTable(emails, folder, meta = {}) {
|
|
|
187
187
|
const date = formatDateShort(email.receivedDateTime);
|
|
188
188
|
const status = email.isRead ? '📖' : '📬';
|
|
189
189
|
const subject = truncateText(email.subject || '(no subject)', 40);
|
|
190
|
-
const shortId = email.id.substring(0, 20)
|
|
190
|
+
const shortId = `${email.id.substring(0, 20)}...`;
|
|
191
191
|
|
|
192
192
|
output += `| ${i + 1} | ${status} | ${date} | ${truncateText(from, 20)} | ${subject} | \`${shortId}\` |\n`;
|
|
193
193
|
});
|
|
@@ -237,10 +237,12 @@ function formatEmailContent(
|
|
|
237
237
|
|
|
238
238
|
if (verbosity === VERBOSITY.FULL) {
|
|
239
239
|
output += `\n**ID:** \`${email.id}\``;
|
|
240
|
-
if (email.conversationId)
|
|
240
|
+
if (email.conversationId) {
|
|
241
241
|
output += `\n**Conversation ID:** \`${email.conversationId}\``;
|
|
242
|
-
|
|
242
|
+
}
|
|
243
|
+
if (email.internetMessageId) {
|
|
243
244
|
output += `\n**Message-ID:** \`${email.internetMessageId}\``;
|
|
245
|
+
}
|
|
244
246
|
}
|
|
245
247
|
|
|
246
248
|
output += '\n\n---\n\n';
|
|
@@ -457,7 +459,7 @@ function formatRecipients(recipients) {
|
|
|
457
459
|
function truncateText(text, maxLength) {
|
|
458
460
|
if (!text) return '';
|
|
459
461
|
if (text.length <= maxLength) return text;
|
|
460
|
-
return text.substring(0, maxLength - 3)
|
|
462
|
+
return `${text.substring(0, maxLength - 3)}...`;
|
|
461
463
|
}
|
|
462
464
|
|
|
463
465
|
/**
|
|
@@ -490,10 +492,10 @@ function escapeCSV(value) {
|
|
|
490
492
|
const isFormula = formulaChars.some((ch) => str.startsWith(ch));
|
|
491
493
|
|
|
492
494
|
if (isFormula) {
|
|
493
|
-
return
|
|
495
|
+
return `"` + `'${str.replace(/"/g, '""')}"`;
|
|
494
496
|
}
|
|
495
497
|
if (needsQuoting) {
|
|
496
|
-
return
|
|
498
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
497
499
|
}
|
|
498
500
|
return str;
|
|
499
501
|
}
|