@littlebearapps/outlook-assistant 3.13.0 → 3.14.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/.env.example +30 -3
- package/README.md +67 -27
- package/advanced/index.js +44 -174
- package/auth/auth-errors.js +23 -1
- package/auth/oauth-server.js +7 -1
- package/auth/token-manager.js +7 -3
- package/auth/token-storage.js +28 -30
- package/auth/tools.js +61 -82
- 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 +461 -0
- package/calendar/update.js +55 -83
- package/categories/index.js +68 -265
- package/config.js +29 -1
- package/contacts/index.js +72 -128
- package/email/attachments.js +43 -125
- package/email/conversations.js +44 -78
- package/email/delta.js +69 -46
- package/email/draft.js +170 -103
- package/email/export.js +145 -110
- package/email/folder-utils.js +3 -2
- package/email/headers.js +11 -49
- package/email/index.js +86 -110
- 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 +39 -51
- package/email/read.js +16 -50
- package/email/search.js +47 -87
- package/email/send.js +82 -48
- package/folder/create.js +6 -25
- package/folder/delete.js +117 -38
- package/folder/index.js +19 -17
- package/folder/list.js +5 -17
- package/folder/move.js +13 -42
- package/folder/resolve.js +11 -6
- package/folder/stats.js +18 -27
- package/index.js +39 -45
- package/llms-install.md +22 -4
- package/llms.txt +20 -11
- package/outlook-auth-server.js +10 -3
- package/package.json +4 -1
- package/request-handler.js +217 -116
- package/rules/create.js +28 -71
- package/rules/index.js +52 -93
- package/rules/list.js +7 -19
- package/rules/rule-builder.js +59 -22
- package/rules/update.js +27 -61
- package/server.js +41 -0
- package/settings/index.js +162 -145
- 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 +247 -42
- package/utils/server-instructions.js +73 -0
- package/utils/tool-error.js +33 -0
package/email/draft.js
CHANGED
|
@@ -8,10 +8,14 @@ const { callGraphAPI } = require('../utils/graph-api');
|
|
|
8
8
|
const { ensureAuthenticated } = require('../auth');
|
|
9
9
|
const {
|
|
10
10
|
checkRateLimit,
|
|
11
|
+
releaseRateLimit,
|
|
11
12
|
checkRecipientAllowlist,
|
|
13
|
+
findBlockedRecipients,
|
|
14
|
+
getRecipientAllowlist,
|
|
12
15
|
formatDryRunPreview,
|
|
13
16
|
} = require('../utils/safety');
|
|
14
17
|
const { handleGetMailTips } = require('./mail-tips');
|
|
18
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
21
|
* Format comma-separated email string into Graph API recipient objects
|
|
@@ -25,6 +29,18 @@ function formatRecipients(recipientString) {
|
|
|
25
29
|
}));
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
/** Graph fields holding a message's recipients. */
|
|
33
|
+
const RECIPIENT_FIELDS = ['toRecipients', 'ccRecipients', 'bccRecipients'];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Every to/cc/bcc recipient on a Graph message.
|
|
37
|
+
* @param {object} message - Graph message
|
|
38
|
+
* @returns {Array<{emailAddress: {address: string}}>}
|
|
39
|
+
*/
|
|
40
|
+
function recipientsOf(message) {
|
|
41
|
+
return RECIPIENT_FIELDS.flatMap((field) => message?.[field] || []);
|
|
42
|
+
}
|
|
43
|
+
|
|
28
44
|
/**
|
|
29
45
|
* Auto-detect HTML vs plain text body
|
|
30
46
|
* @param {string} body - Email body content
|
|
@@ -71,7 +87,7 @@ function buildMessageObject(args) {
|
|
|
71
87
|
/**
|
|
72
88
|
* Format a draft response with key details
|
|
73
89
|
* @param {object} draft - Graph API message response
|
|
74
|
-
* @param {string} actionLabel -
|
|
90
|
+
* @param {string} actionLabel - Heading, e.g. "Draft created" or "Reply draft created"
|
|
75
91
|
* @returns {object} - MCP response
|
|
76
92
|
*/
|
|
77
93
|
function formatDraftResponse(draft, actionLabel) {
|
|
@@ -79,7 +95,7 @@ function formatDraftResponse(draft, actionLabel) {
|
|
|
79
95
|
.map((r) => r.emailAddress?.address)
|
|
80
96
|
.join(', ');
|
|
81
97
|
|
|
82
|
-
let text =
|
|
98
|
+
let text = `${actionLabel}.\n\n`;
|
|
83
99
|
text += `**ID**: \`${draft.id}\`\n`;
|
|
84
100
|
if (draft.subject) text += `**Subject**: ${draft.subject}\n`;
|
|
85
101
|
if (to) text += `**To**: ${to}\n`;
|
|
@@ -108,9 +124,11 @@ class DraftGuardError extends Error {}
|
|
|
108
124
|
* @param {string} accessToken - Graph access token
|
|
109
125
|
* @param {string} id - Message id the caller passed as the draft id
|
|
110
126
|
* @param {string} action - The draft action being guarded (for the message)
|
|
127
|
+
* @param {string[]} [extraFields] - Further fields to fetch in the same GET
|
|
128
|
+
* @returns {Promise<object>} The draft, with id, isDraft, subject and extraFields
|
|
111
129
|
* @throws {DraftGuardError} If the id is not a draft or does not exist
|
|
112
130
|
*/
|
|
113
|
-
async function assertIsDraft(accessToken, id, action) {
|
|
131
|
+
async function assertIsDraft(accessToken, id, action, extraFields = []) {
|
|
114
132
|
let message;
|
|
115
133
|
try {
|
|
116
134
|
message = await callGraphAPI(
|
|
@@ -119,7 +137,7 @@ async function assertIsDraft(accessToken, id, action) {
|
|
|
119
137
|
`me/messages/${id}`,
|
|
120
138
|
null,
|
|
121
139
|
{
|
|
122
|
-
$select: 'id,isDraft,subject',
|
|
140
|
+
$select: ['id', 'isDraft', 'subject', ...extraFields].join(','),
|
|
123
141
|
}
|
|
124
142
|
);
|
|
125
143
|
} catch (error) {
|
|
@@ -137,6 +155,7 @@ async function assertIsDraft(accessToken, id, action) {
|
|
|
137
155
|
`Message \`${id}\`${subject} is not a draft, so draft action=${action} refused it and nothing was changed. update/send/delete only act on unsent drafts.`
|
|
138
156
|
);
|
|
139
157
|
}
|
|
158
|
+
return message;
|
|
140
159
|
}
|
|
141
160
|
|
|
142
161
|
/**
|
|
@@ -148,14 +167,9 @@ async function handleDraft(args) {
|
|
|
148
167
|
const { action } = args;
|
|
149
168
|
|
|
150
169
|
if (!action) {
|
|
151
|
-
return
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
type: 'text',
|
|
155
|
-
text: "Action is required. Use 'create', 'update', 'send', 'delete', 'reply', 'reply-all', or 'forward'.",
|
|
156
|
-
},
|
|
157
|
-
],
|
|
158
|
-
};
|
|
170
|
+
return toolError(
|
|
171
|
+
"Action is required. Use 'create', 'update', 'send', 'delete', 'reply', 'reply-all', or 'forward'."
|
|
172
|
+
);
|
|
159
173
|
}
|
|
160
174
|
|
|
161
175
|
switch (action) {
|
|
@@ -174,14 +188,9 @@ async function handleDraft(args) {
|
|
|
174
188
|
case 'forward':
|
|
175
189
|
return handleForwardDraft(args);
|
|
176
190
|
default:
|
|
177
|
-
return
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
type: 'text',
|
|
181
|
-
text: `Invalid action '${action}'. Use 'create', 'update', 'send', 'delete', 'reply', 'reply-all', or 'forward'.`,
|
|
182
|
-
},
|
|
183
|
-
],
|
|
184
|
-
};
|
|
191
|
+
return toolError(
|
|
192
|
+
`Invalid action '${action}'. Use 'create', 'update', 'send', 'delete', 'reply', 'reply-all', or 'forward'.`
|
|
193
|
+
);
|
|
185
194
|
}
|
|
186
195
|
}
|
|
187
196
|
|
|
@@ -203,14 +212,16 @@ async function handleCreateDraft(args) {
|
|
|
203
212
|
if (allowlistError) return allowlistError;
|
|
204
213
|
}
|
|
205
214
|
|
|
206
|
-
// Pre-save recipient validation via mail-tips
|
|
215
|
+
// Pre-save recipient validation via mail-tips. The tips are returned
|
|
216
|
+
// with the result, never added to the draft payload (#272).
|
|
217
|
+
let tipsResult = null;
|
|
207
218
|
if (doCheckRecipients && allRecipients.length > 0) {
|
|
208
219
|
const allAddresses = allRecipients.map((r) => r.emailAddress.address);
|
|
209
|
-
|
|
220
|
+
tipsResult = await handleGetMailTips({ recipients: allAddresses });
|
|
210
221
|
const tipsText = tipsResult.content[0]?.text || '';
|
|
211
222
|
|
|
212
223
|
if (dryRun) {
|
|
213
|
-
const preview = formatDryRunPreview({ message,
|
|
224
|
+
const preview = formatDryRunPreview({ message, isDraft: true });
|
|
214
225
|
return {
|
|
215
226
|
content: [
|
|
216
227
|
{
|
|
@@ -228,7 +239,7 @@ async function handleCreateDraft(args) {
|
|
|
228
239
|
|
|
229
240
|
// Dry-run mode: preview without saving
|
|
230
241
|
if (dryRun) {
|
|
231
|
-
const preview = formatDryRunPreview({ message,
|
|
242
|
+
const preview = formatDryRunPreview({ message, isDraft: true });
|
|
232
243
|
return {
|
|
233
244
|
content: [
|
|
234
245
|
{
|
|
@@ -254,7 +265,12 @@ async function handleCreateDraft(args) {
|
|
|
254
265
|
'me/messages',
|
|
255
266
|
message
|
|
256
267
|
);
|
|
257
|
-
|
|
268
|
+
const response = formatDraftResponse(draft, 'Draft created');
|
|
269
|
+
if (tipsResult) {
|
|
270
|
+
response.content[0].text += `\n---\n\n${tipsResult.content[0]?.text || ''}`;
|
|
271
|
+
response._meta.mailTips = tipsResult._meta;
|
|
272
|
+
}
|
|
273
|
+
return response;
|
|
258
274
|
} catch (error) {
|
|
259
275
|
return handleError('creating draft', error);
|
|
260
276
|
}
|
|
@@ -267,11 +283,7 @@ async function handleUpdateDraft(args) {
|
|
|
267
283
|
const { id } = args;
|
|
268
284
|
|
|
269
285
|
if (!id) {
|
|
270
|
-
return
|
|
271
|
-
content: [
|
|
272
|
-
{ type: 'text', text: 'Draft ID (id) is required for update.' },
|
|
273
|
-
],
|
|
274
|
-
};
|
|
286
|
+
return toolError('Draft ID (id) is required for update.');
|
|
275
287
|
}
|
|
276
288
|
|
|
277
289
|
const message = buildMessageObject(args);
|
|
@@ -301,7 +313,7 @@ async function handleUpdateDraft(args) {
|
|
|
301
313
|
`me/messages/${id}`,
|
|
302
314
|
message
|
|
303
315
|
);
|
|
304
|
-
return formatDraftResponse(draft, 'updated');
|
|
316
|
+
return formatDraftResponse(draft, 'Draft updated');
|
|
305
317
|
} catch (error) {
|
|
306
318
|
return handleError('updating draft', error);
|
|
307
319
|
}
|
|
@@ -314,14 +326,29 @@ async function handleSendDraft(args) {
|
|
|
314
326
|
const { id } = args;
|
|
315
327
|
|
|
316
328
|
if (!id) {
|
|
317
|
-
return
|
|
318
|
-
content: [{ type: 'text', text: 'Draft ID (id) is required for send.' }],
|
|
319
|
-
};
|
|
329
|
+
return toolError('Draft ID (id) is required for send.');
|
|
320
330
|
}
|
|
321
331
|
|
|
322
332
|
try {
|
|
323
333
|
const accessToken = await ensureAuthenticated();
|
|
324
|
-
|
|
334
|
+
// Fetch the recipients as they are now: the draft may have been edited
|
|
335
|
+
// (here or in Outlook) since it was created.
|
|
336
|
+
const draft = await assertIsDraft(
|
|
337
|
+
accessToken,
|
|
338
|
+
id,
|
|
339
|
+
'send',
|
|
340
|
+
RECIPIENT_FIELDS
|
|
341
|
+
);
|
|
342
|
+
const blocked = findBlockedRecipients(recipientsOf(draft));
|
|
343
|
+
if (blocked) {
|
|
344
|
+
return toolError(
|
|
345
|
+
`Draft not sent: it is addressed to ${blocked.blocked.join(', ')}, which OUTLOOK_ALLOWED_RECIPIENTS does not allow (allowed recipients/domains: ${blocked.allowed.join(', ')}). The draft is unchanged and still in Drafts.`,
|
|
346
|
+
{
|
|
347
|
+
nextStep:
|
|
348
|
+
'Remove those recipients from the draft (in Outlook, or with draft action=update), or ask the user to add them to OUTLOOK_ALLOWED_RECIPIENTS.',
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
}
|
|
325
352
|
|
|
326
353
|
// Rate limit via send-email counter (shares limit with direct sends)
|
|
327
354
|
const rateLimitError = checkRateLimit('send-email');
|
|
@@ -348,11 +375,7 @@ async function handleDeleteDraft(args) {
|
|
|
348
375
|
const { id } = args;
|
|
349
376
|
|
|
350
377
|
if (!id) {
|
|
351
|
-
return
|
|
352
|
-
content: [
|
|
353
|
-
{ type: 'text', text: 'Draft ID (id) is required for delete.' },
|
|
354
|
-
],
|
|
355
|
-
};
|
|
378
|
+
return toolError('Draft ID (id) is required for delete.');
|
|
356
379
|
}
|
|
357
380
|
|
|
358
381
|
try {
|
|
@@ -379,25 +402,15 @@ async function handleReplyDraft(args, endpoint) {
|
|
|
379
402
|
const { id, body, comment } = args;
|
|
380
403
|
|
|
381
404
|
if (!id) {
|
|
382
|
-
return
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
type: 'text',
|
|
386
|
-
text: `Message ID (id) is required for ${endpoint === 'createReplyAll' ? 'reply-all' : 'reply'}.`,
|
|
387
|
-
},
|
|
388
|
-
],
|
|
389
|
-
};
|
|
405
|
+
return toolError(
|
|
406
|
+
`Message ID (id) is required for ${endpoint === 'createReplyAll' ? 'reply-all' : 'reply'}.`
|
|
407
|
+
);
|
|
390
408
|
}
|
|
391
409
|
|
|
392
410
|
if (comment && body) {
|
|
393
|
-
return
|
|
394
|
-
content
|
|
395
|
-
|
|
396
|
-
type: 'text',
|
|
397
|
-
text: 'Cannot use both comment and body. Use comment for a short prepended note, or body for full HTML/text content.',
|
|
398
|
-
},
|
|
399
|
-
],
|
|
400
|
-
};
|
|
411
|
+
return toolError(
|
|
412
|
+
'Cannot use both comment and body. Use comment for a short prepended note, or body for full HTML/text content.'
|
|
413
|
+
);
|
|
401
414
|
}
|
|
402
415
|
|
|
403
416
|
const requestBody = {};
|
|
@@ -412,25 +425,106 @@ async function handleReplyDraft(args, endpoint) {
|
|
|
412
425
|
};
|
|
413
426
|
}
|
|
414
427
|
|
|
428
|
+
const actionName = endpoint === 'createReplyAll' ? 'reply-all' : 'reply';
|
|
429
|
+
|
|
430
|
+
// Taken before the draft is created, and given back only when nothing is
|
|
431
|
+
// definitely left behind (#299): Graph rejected the create with a 4xx, or
|
|
432
|
+
// the allowlist refused the reply and its draft was deleted again. A
|
|
433
|
+
// timeout, 5xx or failed delete may leave a draft, so the slot stays used.
|
|
434
|
+
const rateLimitError = checkRateLimit('draft');
|
|
435
|
+
if (rateLimitError) return rateLimitError;
|
|
436
|
+
|
|
437
|
+
let draft;
|
|
415
438
|
try {
|
|
416
439
|
const accessToken = await ensureAuthenticated();
|
|
417
|
-
|
|
440
|
+
draft = await callGraphAPI(
|
|
418
441
|
accessToken,
|
|
419
442
|
'POST',
|
|
420
443
|
`me/messages/${id}/${endpoint}`,
|
|
421
444
|
Object.keys(requestBody).length > 0 ? requestBody : null
|
|
422
445
|
);
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
446
|
+
|
|
447
|
+
// Graph fills in the recipients from the original message, so they can
|
|
448
|
+
// only be checked once the draft exists.
|
|
449
|
+
const refusal = await refuseBlockedReply(accessToken, draft, actionName);
|
|
450
|
+
if (refusal) {
|
|
451
|
+
if (refusal.draftDeleted) releaseRateLimit('draft');
|
|
452
|
+
return refusal.error;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return formatDraftResponse(
|
|
456
|
+
draft,
|
|
457
|
+
`${actionName.charAt(0).toUpperCase()}${actionName.slice(1)} draft created`
|
|
458
|
+
);
|
|
459
|
+
} catch (error) {
|
|
460
|
+
// Only Graph's own rejection, read from the start of the error (the
|
|
461
|
+
// body after it is server text). 408 means it may have gone through.
|
|
462
|
+
const rejected = /^API call failed with status (4\d\d):/.exec(
|
|
463
|
+
error.message || ''
|
|
464
|
+
);
|
|
465
|
+
if (!draft && rejected && rejected[1] !== '408') {
|
|
466
|
+
releaseRateLimit('draft');
|
|
467
|
+
}
|
|
468
|
+
return handleError(`creating ${actionName} draft`, error);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* When a recipient allowlist is configured, check the recipients Graph put
|
|
474
|
+
* on a new reply draft. If any is not allowed, delete the draft and return a
|
|
475
|
+
* refusal; otherwise return null.
|
|
476
|
+
* @param {string} accessToken - Graph access token
|
|
477
|
+
* @param {object} draft - The draft Graph returned from createReply/createReplyAll
|
|
478
|
+
* @param {string} actionName - 'reply' or 'reply-all'
|
|
479
|
+
* @returns {Promise<{error: object, draftDeleted: boolean}|null>} The
|
|
480
|
+
* refusal and whether its draft was deleted, or null to keep the draft
|
|
481
|
+
*/
|
|
482
|
+
async function refuseBlockedReply(accessToken, draft, actionName) {
|
|
483
|
+
if (!getRecipientAllowlist()) return null;
|
|
484
|
+
|
|
485
|
+
let reason;
|
|
486
|
+
let nextStep;
|
|
487
|
+
try {
|
|
488
|
+
let message = draft;
|
|
489
|
+
if (!RECIPIENT_FIELDS.every((field) => Array.isArray(draft?.[field]))) {
|
|
490
|
+
message = await callGraphAPI(
|
|
491
|
+
accessToken,
|
|
492
|
+
'GET',
|
|
493
|
+
`me/messages/${draft.id}`,
|
|
494
|
+
null,
|
|
495
|
+
{ $select: ['id', ...RECIPIENT_FIELDS].join(',') }
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
const blocked = findBlockedRecipients(recipientsOf(message));
|
|
499
|
+
if (!blocked) return null;
|
|
500
|
+
reason = `The ${actionName} draft would be addressed to ${blocked.blocked.join(', ')}, which OUTLOOK_ALLOWED_RECIPIENTS does not allow (allowed recipients/domains: ${blocked.allowed.join(', ')}).`;
|
|
501
|
+
nextStep =
|
|
502
|
+
'Reply only to allowed recipients (for example, action=reply rather than reply-all, or draft action=create addressed to them), or ask the user to add those recipients to OUTLOOK_ALLOWED_RECIPIENTS.';
|
|
503
|
+
} catch (error) {
|
|
504
|
+
// Unchecked recipients are treated like blocked ones.
|
|
505
|
+
reason = `The ${actionName} draft's recipients could not be checked against OUTLOOK_ALLOWED_RECIPIENTS (${error.message}).`;
|
|
506
|
+
nextStep = 'Try the call again.';
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
await callGraphAPI(accessToken, 'DELETE', `me/messages/${draft.id}`);
|
|
428
511
|
} catch (error) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
512
|
+
const stillThere = toolError(
|
|
513
|
+
`${reason} The draft Graph created could not be deleted (${error.message}), so it is still in Drafts with ID \`${draft.id}\`. Do not send it.`,
|
|
514
|
+
{
|
|
515
|
+
nextStep: `Delete it with draft action=delete id=${draft.id} (or in Outlook). ${nextStep}`,
|
|
516
|
+
}
|
|
432
517
|
);
|
|
518
|
+
return { error: stillThere, draftDeleted: false };
|
|
433
519
|
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
error: toolError(
|
|
523
|
+
`${reason} The draft Graph created was deleted, so nothing was kept.`,
|
|
524
|
+
{ nextStep }
|
|
525
|
+
),
|
|
526
|
+
draftDeleted: true,
|
|
527
|
+
};
|
|
434
528
|
}
|
|
435
529
|
|
|
436
530
|
/**
|
|
@@ -440,33 +534,17 @@ async function handleForwardDraft(args) {
|
|
|
440
534
|
const { id, to, body, comment } = args;
|
|
441
535
|
|
|
442
536
|
if (!id) {
|
|
443
|
-
return
|
|
444
|
-
content: [
|
|
445
|
-
{ type: 'text', text: 'Message ID (id) is required for forward.' },
|
|
446
|
-
],
|
|
447
|
-
};
|
|
537
|
+
return toolError('Message ID (id) is required for forward.');
|
|
448
538
|
}
|
|
449
539
|
|
|
450
540
|
if (!to) {
|
|
451
|
-
return
|
|
452
|
-
content: [
|
|
453
|
-
{
|
|
454
|
-
type: 'text',
|
|
455
|
-
text: 'Forward recipient (to) is required for forward.',
|
|
456
|
-
},
|
|
457
|
-
],
|
|
458
|
-
};
|
|
541
|
+
return toolError('Forward recipient (to) is required for forward.');
|
|
459
542
|
}
|
|
460
543
|
|
|
461
544
|
if (comment && body) {
|
|
462
|
-
return
|
|
463
|
-
content
|
|
464
|
-
|
|
465
|
-
type: 'text',
|
|
466
|
-
text: 'Cannot use both comment and body. Use comment for a short prepended note, or body for full HTML/text content.',
|
|
467
|
-
},
|
|
468
|
-
],
|
|
469
|
-
};
|
|
545
|
+
return toolError(
|
|
546
|
+
'Cannot use both comment and body. Use comment for a short prepended note, or body for full HTML/text content.'
|
|
547
|
+
);
|
|
470
548
|
}
|
|
471
549
|
|
|
472
550
|
const toRecipients = formatRecipients(to);
|
|
@@ -475,6 +553,9 @@ async function handleForwardDraft(args) {
|
|
|
475
553
|
const allowlistError = checkRecipientAllowlist(toRecipients);
|
|
476
554
|
if (allowlistError) return allowlistError;
|
|
477
555
|
|
|
556
|
+
const rateLimitError = checkRateLimit('draft');
|
|
557
|
+
if (rateLimitError) return rateLimitError;
|
|
558
|
+
|
|
478
559
|
const requestBody = {
|
|
479
560
|
toRecipients,
|
|
480
561
|
};
|
|
@@ -498,7 +579,7 @@ async function handleForwardDraft(args) {
|
|
|
498
579
|
`me/messages/${id}/createForward`,
|
|
499
580
|
requestBody
|
|
500
581
|
);
|
|
501
|
-
return formatDraftResponse(draft, '
|
|
582
|
+
return formatDraftResponse(draft, 'Forward draft created');
|
|
502
583
|
} catch (error) {
|
|
503
584
|
return handleError('creating forward draft', error);
|
|
504
585
|
}
|
|
@@ -516,24 +597,10 @@ function handleError(actionLabel, error) {
|
|
|
516
597
|
}
|
|
517
598
|
|
|
518
599
|
if (error.message === 'Authentication required') {
|
|
519
|
-
return
|
|
520
|
-
content: [
|
|
521
|
-
{
|
|
522
|
-
type: 'text',
|
|
523
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
524
|
-
},
|
|
525
|
-
],
|
|
526
|
-
};
|
|
600
|
+
return authRequiredError();
|
|
527
601
|
}
|
|
528
602
|
|
|
529
|
-
return {
|
|
530
|
-
content: [
|
|
531
|
-
{
|
|
532
|
-
type: 'text',
|
|
533
|
-
text: `Error ${actionLabel}: ${error.message}`,
|
|
534
|
-
},
|
|
535
|
-
],
|
|
536
|
-
};
|
|
603
|
+
return toolError(`Error ${actionLabel}: ${error.message}`);
|
|
537
604
|
}
|
|
538
605
|
|
|
539
606
|
module.exports = handleDraft;
|