@littlebearapps/outlook-assistant 3.13.0 → 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 +66 -26
- 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 +45 -76
- 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 +29 -1
- 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 +19 -43
- package/llms-install.md +22 -4
- package/llms.txt +17 -8
- package/outlook-auth-server.js +10 -3
- package/package.json +4 -1
- 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/email/export.js
CHANGED
|
@@ -20,7 +20,18 @@ const { resolveFolderPath } = require('./folder-utils');
|
|
|
20
20
|
const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
21
21
|
const { quoteSearchPhrase } = require('../utils/odata-helpers');
|
|
22
22
|
const { safeAttachmentFilename } = require('./attachments');
|
|
23
|
-
const {
|
|
23
|
+
const {
|
|
24
|
+
writeClaimedFile,
|
|
25
|
+
ensureOutputDir,
|
|
26
|
+
writeExplicitFile,
|
|
27
|
+
confineOutputPath,
|
|
28
|
+
confineOutputTarget,
|
|
29
|
+
fileExistsError,
|
|
30
|
+
pathEntryExists,
|
|
31
|
+
OutputPathError,
|
|
32
|
+
} = require('../utils/safe-write');
|
|
33
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
34
|
+
const { log } = require('../utils/logger');
|
|
24
35
|
|
|
25
36
|
// Export format constants
|
|
26
37
|
const EXPORT_FORMATS = {
|
|
@@ -36,31 +47,55 @@ const EXPORT_FORMATS = {
|
|
|
36
47
|
* @param {object} args - Tool arguments
|
|
37
48
|
* @param {string} args.id - Email ID (required)
|
|
38
49
|
* @param {string} [args.format] - Export format (mime, eml, markdown, json)
|
|
39
|
-
* @param {string} [args.savePath] - File path to save (optional)
|
|
50
|
+
* @param {string} [args.savePath] - File path or directory to save to (optional)
|
|
51
|
+
* @param {string} [args.outputDir] - Directory to save to (optional)
|
|
52
|
+
* @param {boolean} [args.overwrite] - Replace an existing savePath file (default: false)
|
|
40
53
|
* @param {boolean} [args.includeAttachments] - Include attachments (default: true)
|
|
41
54
|
* @returns {object} - MCP response with export status
|
|
42
55
|
*/
|
|
43
56
|
async function handleExportEmail(args) {
|
|
44
57
|
const emailId = args.id;
|
|
45
58
|
const format = (args.format || EXPORT_FORMATS.MARKDOWN).toLowerCase();
|
|
46
|
-
// F-27: accept `outputDir` (canonical) and `savePath` (legacy alias).
|
|
47
|
-
// Previously single-message exports ignored outputDir entirely and
|
|
48
|
-
// hardcoded os.tmpdir(), inconsistent with target=messages.
|
|
49
|
-
const savePath = args.outputDir || args.savePath;
|
|
50
59
|
const includeAttachments = args.includeAttachments !== false;
|
|
60
|
+
const overwrite = args.overwrite === true;
|
|
51
61
|
// Message IDs are mailbox-scoped: route to /users/{mailbox} for a shared/
|
|
52
62
|
// delegated mailbox, else /me.
|
|
53
63
|
const prefix = buildMailboxPrefix(args.sharedMailbox || args.email || null);
|
|
54
64
|
|
|
55
65
|
if (!emailId) {
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
return toolError('Email ID is required.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Where to write, checked before anything is fetched. F-27: `outputDir`
|
|
70
|
+
// is always a directory. `savePath` names a directory if one exists
|
|
71
|
+
// there, otherwise the file to write. With no path, the system temp
|
|
72
|
+
// directory is used. Writes go to the resolved path, never the raw one.
|
|
73
|
+
let explicitFile = null;
|
|
74
|
+
let requestedFile = null; // savePath as given, for messages
|
|
75
|
+
let explicitBase = null; // allowed directory it is in
|
|
76
|
+
let targetDir;
|
|
77
|
+
try {
|
|
78
|
+
if (args.outputDir) {
|
|
79
|
+
targetDir = confineOutputPath(args.outputDir);
|
|
80
|
+
} else if (args.savePath) {
|
|
81
|
+
const target = confineOutputTarget(args.savePath);
|
|
82
|
+
const resolved = target.path;
|
|
83
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
84
|
+
targetDir = resolved;
|
|
85
|
+
} else {
|
|
86
|
+
explicitFile = resolved;
|
|
87
|
+
requestedFile = target.requested;
|
|
88
|
+
explicitBase = target.base;
|
|
89
|
+
// Refuse early, before fetching; writeExplicitFile checks again.
|
|
90
|
+
if (!overwrite && pathEntryExists(explicitFile)) {
|
|
91
|
+
throw fileExistsError(requestedFile);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
targetDir = confineOutputPath(os.tmpdir());
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return outputPathError(error);
|
|
64
99
|
}
|
|
65
100
|
|
|
66
101
|
try {
|
|
@@ -77,14 +112,7 @@ async function handleExportEmail(args) {
|
|
|
77
112
|
);
|
|
78
113
|
|
|
79
114
|
if (!email) {
|
|
80
|
-
return {
|
|
81
|
-
content: [
|
|
82
|
-
{
|
|
83
|
-
type: 'text',
|
|
84
|
-
text: `Email with ID ${emailId} not found.`,
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
};
|
|
115
|
+
return toolError(`Email with ID ${emailId} not found.`);
|
|
88
116
|
}
|
|
89
117
|
|
|
90
118
|
// Generate filename based on email metadata. The time matters: a
|
|
@@ -98,16 +126,6 @@ async function handleExportEmail(args) {
|
|
|
98
126
|
// Paths claimed while writing this message (main file + attachments).
|
|
99
127
|
const claimedPaths = new Set();
|
|
100
128
|
|
|
101
|
-
// Determine the save location. An explicit file path is the caller's to
|
|
102
|
-
// control — honour it exactly, including overwriting, since that is what
|
|
103
|
-
// an explicit path means. A directory (or the default temp dir) means we
|
|
104
|
-
// choose the name, so the write is exclusive (`wx`): it never clobbers an
|
|
105
|
-
// existing file and never follows a planted symlink.
|
|
106
|
-
const explicitFile =
|
|
107
|
-
savePath &&
|
|
108
|
-
!(fs.existsSync(savePath) && fs.statSync(savePath).isDirectory());
|
|
109
|
-
const targetDir = explicitFile ? null : savePath || os.tmpdir();
|
|
110
|
-
|
|
111
129
|
// Export based on format
|
|
112
130
|
let content;
|
|
113
131
|
let attachmentsSaved = [];
|
|
@@ -130,34 +148,35 @@ async function handleExportEmail(args) {
|
|
|
130
148
|
} else if (format === 'mbox' || format === 'html') {
|
|
131
149
|
// F-26: clarify that mbox/html are conversation-only formats so
|
|
132
150
|
// callers don't infer the format itself is unsupported.
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
type: 'text',
|
|
137
|
-
text: `Format '${format}' is only supported for target=conversation. For target=message use one of: ${Object.values(EXPORT_FORMATS).join(', ')}.`,
|
|
138
|
-
},
|
|
139
|
-
],
|
|
140
|
-
};
|
|
151
|
+
return toolError(
|
|
152
|
+
`Format '${format}' is only supported for target=conversation. For target=message use one of: ${Object.values(EXPORT_FORMATS).join(', ')}.`
|
|
153
|
+
);
|
|
141
154
|
} else {
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
type: 'text',
|
|
146
|
-
text: `Unknown format: ${format}. Supported for target=message: ${Object.values(EXPORT_FORMATS).join(', ')}.`,
|
|
147
|
-
},
|
|
148
|
-
],
|
|
149
|
-
};
|
|
155
|
+
return toolError(
|
|
156
|
+
`Unknown format: ${format}. Supported for target=message: ${Object.values(EXPORT_FORMATS).join(', ')}.`
|
|
157
|
+
);
|
|
150
158
|
}
|
|
151
159
|
|
|
152
160
|
// Save main file. Auto-create the directory so callers don't have to
|
|
153
|
-
// pre-mkdir.
|
|
161
|
+
// pre-mkdir. An explicit file is created exclusively and replaces an
|
|
162
|
+
// existing file only with overwrite: true. A directory means we choose
|
|
163
|
+
// the name, so the write is exclusive (`wx`): it never clobbers an
|
|
164
|
+
// existing file and never follows a planted symlink.
|
|
154
165
|
let finalPath;
|
|
166
|
+
let replaced = false;
|
|
155
167
|
if (explicitFile) {
|
|
156
|
-
finalPath =
|
|
157
|
-
|
|
158
|
-
|
|
168
|
+
({ path: finalPath, replaced } = writeExplicitFile(
|
|
169
|
+
explicitFile,
|
|
170
|
+
content,
|
|
171
|
+
{
|
|
172
|
+
overwrite,
|
|
173
|
+
encoding: 'utf8',
|
|
174
|
+
displayPath: requestedFile,
|
|
175
|
+
base: explicitBase,
|
|
176
|
+
}
|
|
177
|
+
));
|
|
159
178
|
} else {
|
|
160
|
-
|
|
179
|
+
ensureOutputDir(targetDir);
|
|
161
180
|
finalPath = writeClaimedFile(
|
|
162
181
|
targetDir,
|
|
163
182
|
defaultBase,
|
|
@@ -184,6 +203,7 @@ async function handleExportEmail(args) {
|
|
|
184
203
|
resultText += `| Property | Value |\n`;
|
|
185
204
|
resultText += `|----------|-------|\n`;
|
|
186
205
|
resultText += `| File | \`${finalPath}\` |\n`;
|
|
206
|
+
if (replaced) resultText += `| Replaced | yes (existing file) |\n`;
|
|
187
207
|
resultText += `| Format | ${format.toUpperCase()} |\n`;
|
|
188
208
|
resultText += `| Size | ${content.length.toLocaleString()} bytes |\n`;
|
|
189
209
|
resultText += `| Subject | ${email.subject} |\n`;
|
|
@@ -206,6 +226,7 @@ async function handleExportEmail(args) {
|
|
|
206
226
|
],
|
|
207
227
|
_meta: {
|
|
208
228
|
filePath: finalPath,
|
|
229
|
+
replaced,
|
|
209
230
|
format: format,
|
|
210
231
|
sizeBytes: content.length,
|
|
211
232
|
attachmentsSaved: attachmentsSaved.length,
|
|
@@ -213,28 +234,27 @@ async function handleExportEmail(args) {
|
|
|
213
234
|
},
|
|
214
235
|
};
|
|
215
236
|
} catch (error) {
|
|
237
|
+
if (error instanceof OutputPathError) {
|
|
238
|
+
return outputPathError(error);
|
|
239
|
+
}
|
|
216
240
|
if (error.message === 'Authentication required') {
|
|
217
|
-
return
|
|
218
|
-
content: [
|
|
219
|
-
{
|
|
220
|
-
type: 'text',
|
|
221
|
-
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
222
|
-
},
|
|
223
|
-
],
|
|
224
|
-
};
|
|
241
|
+
return authRequiredError();
|
|
225
242
|
}
|
|
226
243
|
|
|
227
|
-
return {
|
|
228
|
-
content: [
|
|
229
|
-
{
|
|
230
|
-
type: 'text',
|
|
231
|
-
text: `Export failed: ${error.message}`,
|
|
232
|
-
},
|
|
233
|
-
],
|
|
234
|
-
};
|
|
244
|
+
return toolError(`Export failed: ${error.message}`);
|
|
235
245
|
}
|
|
236
246
|
}
|
|
237
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Tool error for a refused output path; rethrows anything else.
|
|
250
|
+
* @param {Error} error
|
|
251
|
+
* @returns {object} MCP error response
|
|
252
|
+
*/
|
|
253
|
+
function outputPathError(error) {
|
|
254
|
+
if (!(error instanceof OutputPathError)) throw error;
|
|
255
|
+
return toolError(error.message, { nextStep: error.nextStep });
|
|
256
|
+
}
|
|
257
|
+
|
|
238
258
|
/**
|
|
239
259
|
* Batch export emails handler
|
|
240
260
|
* @param {object} args - Tool arguments
|
|
@@ -255,28 +275,25 @@ async function handleBatchExportEmails(args) {
|
|
|
255
275
|
searchQuery.subject = args.query;
|
|
256
276
|
}
|
|
257
277
|
const format = (args.format || EXPORT_FORMATS.MARKDOWN).toLowerCase();
|
|
258
|
-
const outputDir = args.outputDir;
|
|
259
278
|
const includeAttachments = args.includeAttachments === true; // Default false for batch
|
|
260
279
|
// Scope the whole batch (search + per-message fetch + attachments) to a
|
|
261
280
|
// shared/delegated mailbox when supplied.
|
|
262
281
|
const mailbox = args.sharedMailbox || args.email || null;
|
|
263
282
|
const prefix = buildMailboxPrefix(mailbox);
|
|
264
283
|
|
|
265
|
-
if (!outputDir) {
|
|
266
|
-
return
|
|
267
|
-
content: [
|
|
268
|
-
{
|
|
269
|
-
type: 'text',
|
|
270
|
-
text: 'Output directory is required.',
|
|
271
|
-
},
|
|
272
|
-
],
|
|
273
|
-
};
|
|
284
|
+
if (!args.outputDir) {
|
|
285
|
+
return toolError('Output directory is required.');
|
|
274
286
|
}
|
|
275
287
|
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
288
|
+
// Resolve and check the directory before creating it; write only to the
|
|
289
|
+
// resolved path.
|
|
290
|
+
let outputDir;
|
|
291
|
+
try {
|
|
292
|
+
outputDir = confineOutputPath(args.outputDir);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
return outputPathError(error);
|
|
279
295
|
}
|
|
296
|
+
ensureOutputDir(outputDir);
|
|
280
297
|
|
|
281
298
|
try {
|
|
282
299
|
const accessToken = await ensureAuthenticated();
|
|
@@ -290,24 +307,29 @@ async function handleBatchExportEmails(args) {
|
|
|
290
307
|
mailbox
|
|
291
308
|
);
|
|
292
309
|
idsToExport = searchResults.map((e) => e.id);
|
|
310
|
+
// An empty match is a result, not an error.
|
|
311
|
+
if (idsToExport.length === 0) {
|
|
312
|
+
return {
|
|
313
|
+
content: [
|
|
314
|
+
{
|
|
315
|
+
type: 'text',
|
|
316
|
+
text: 'No emails matched the search query; nothing was exported.',
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
};
|
|
320
|
+
}
|
|
293
321
|
}
|
|
294
322
|
|
|
295
323
|
if (idsToExport.length === 0) {
|
|
296
|
-
return
|
|
297
|
-
content: [
|
|
298
|
-
{
|
|
299
|
-
type: 'text',
|
|
300
|
-
text: 'No emails to export. Provide emailIds or searchQuery.',
|
|
301
|
-
},
|
|
302
|
-
],
|
|
303
|
-
};
|
|
324
|
+
return toolError('No emails to export. Provide emailIds or searchQuery.');
|
|
304
325
|
}
|
|
305
326
|
|
|
306
|
-
// Limit batch size (per plan: max 100)
|
|
327
|
+
// Limit batch size (per plan: max 100), and say so in the result (#279)
|
|
307
328
|
const maxBatch = 100;
|
|
329
|
+
const limitNote = batchLimitNote(idsToExport.length, emailIds, searchQuery);
|
|
308
330
|
if (idsToExport.length > maxBatch) {
|
|
309
331
|
idsToExport = idsToExport.slice(0, maxBatch);
|
|
310
|
-
|
|
332
|
+
log.debug(`Batch export limited to ${maxBatch} emails`);
|
|
311
333
|
}
|
|
312
334
|
|
|
313
335
|
// CSV batch export: aggregate all emails into a single CSV file
|
|
@@ -333,7 +355,7 @@ async function handleBatchExportEmails(args) {
|
|
|
333
355
|
|
|
334
356
|
const csvContent = formatEmailsAsCSV(emails);
|
|
335
357
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
336
|
-
|
|
358
|
+
ensureOutputDir(outputDir);
|
|
337
359
|
const csvPath = writeClaimedFile(
|
|
338
360
|
outputDir,
|
|
339
361
|
`batch_export_${timestamp}`,
|
|
@@ -353,6 +375,7 @@ async function handleBatchExportEmails(args) {
|
|
|
353
375
|
resultText += `| Output File | \`${csvPath}\` |\n`;
|
|
354
376
|
resultText += `| Format | CSV |\n`;
|
|
355
377
|
resultText += `| Total Size | ${(totalBytes / 1024).toFixed(1)} KB |\n`;
|
|
378
|
+
resultText += limitNote;
|
|
356
379
|
|
|
357
380
|
if (failed.length > 0) {
|
|
358
381
|
resultText += `\n### Failed Exports\n\n`;
|
|
@@ -407,6 +430,7 @@ async function handleBatchExportEmails(args) {
|
|
|
407
430
|
0
|
|
408
431
|
);
|
|
409
432
|
resultText += `| Total Size | ${(totalBytes / 1024).toFixed(1)} KB |\n`;
|
|
433
|
+
resultText += limitNote;
|
|
410
434
|
|
|
411
435
|
// Requested id -> written path, so a caller can reconcile without
|
|
412
436
|
// listing the directory. A batch that silently lost messages to
|
|
@@ -452,25 +476,32 @@ async function handleBatchExportEmails(args) {
|
|
|
452
476
|
};
|
|
453
477
|
} catch (error) {
|
|
454
478
|
if (error.message === 'Authentication required') {
|
|
455
|
-
return
|
|
456
|
-
content: [
|
|
457
|
-
{
|
|
458
|
-
type: 'text',
|
|
459
|
-
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
460
|
-
},
|
|
461
|
-
],
|
|
462
|
-
};
|
|
479
|
+
return authRequiredError();
|
|
463
480
|
}
|
|
464
481
|
|
|
465
|
-
return {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
482
|
+
return toolError(`Batch export failed: ${error.message}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Says when a batch export was cut short, and how to get the rest (#279).
|
|
488
|
+
* target=messages exports at most 100 messages per call, and a search stops
|
|
489
|
+
* at searchQuery.maxResults (default 25, max 100).
|
|
490
|
+
* @param {number} found - IDs given or matched before the cap
|
|
491
|
+
* @param {string[]} emailIds - IDs the caller passed (empty for a search)
|
|
492
|
+
* @param {object} searchQuery - The search used when emailIds is empty
|
|
493
|
+
* @returns {string} - Markdown note, or '' when nothing was left out
|
|
494
|
+
*/
|
|
495
|
+
function batchLimitNote(found, emailIds, searchQuery) {
|
|
496
|
+
if (emailIds.length > 0) {
|
|
497
|
+
if (found <= 100) return '';
|
|
498
|
+
return `\n> Exported the first 100 of ${found} requested messages (limit 100 per call). Export the remaining ${found - 100} IDs in another call.\n`;
|
|
473
499
|
}
|
|
500
|
+
const searchLimit = Math.min(searchQuery.maxResults || 25, 100);
|
|
501
|
+
if (found < searchLimit) return '';
|
|
502
|
+
const raise =
|
|
503
|
+
searchLimit < 100 ? 'raise `searchQuery.maxResults` (up to 100) or ' : '';
|
|
504
|
+
return `\n> The search stopped at ${searchLimit} messages, the \`searchQuery.maxResults\` limit (default 25, max 100 per call), so more may match. To get the rest, ${raise}export in date ranges with \`searchQuery.receivedAfter\`/\`receivedBefore\`.\n`;
|
|
474
505
|
}
|
|
475
506
|
|
|
476
507
|
/**
|
|
@@ -707,7 +738,7 @@ async function saveAttachments(
|
|
|
707
738
|
}
|
|
708
739
|
}
|
|
709
740
|
} catch (error) {
|
|
710
|
-
|
|
741
|
+
log.debug(`Failed to save attachments: ${error.message}`);
|
|
711
742
|
}
|
|
712
743
|
|
|
713
744
|
return saved;
|
package/email/folder-utils.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
const { resolveFolder, looksLikeFolderId } = require('../folder/resolve');
|
|
5
5
|
const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
6
|
+
const { log } = require('../utils/logger');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Cache of folder information to reduce API calls
|
|
@@ -74,7 +75,7 @@ async function resolveFolderPath(accessToken, folderName, mailbox = null) {
|
|
|
74
75
|
// Check if it's a well-known folder (case-insensitive)
|
|
75
76
|
const lowerFolderName = folderName.toLowerCase();
|
|
76
77
|
if (WELL_KNOWN_FOLDERS[lowerFolderName]) {
|
|
77
|
-
|
|
78
|
+
log.debug(`Using well-known folder path for "${folderName}"`);
|
|
78
79
|
return scope(WELL_KNOWN_FOLDERS[lowerFolderName], prefix);
|
|
79
80
|
}
|
|
80
81
|
|
|
@@ -93,7 +94,7 @@ async function resolveFolderPath(accessToken, folderName, mailbox = null) {
|
|
|
93
94
|
mailbox,
|
|
94
95
|
});
|
|
95
96
|
const path = `${prefix}/mailFolders/${resolved.id}/messages`;
|
|
96
|
-
|
|
97
|
+
log.debug(`Resolved folder "${folderName}" to path: ${path}`);
|
|
97
98
|
return path;
|
|
98
99
|
} catch (error) {
|
|
99
100
|
// Surface not-found / ambiguity messages verbatim; wrap anything else.
|
package/email/headers.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
8
8
|
const { ensureAuthenticated } = require('../auth');
|
|
9
9
|
const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
10
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
11
|
+
const { log } = require('../utils/logger');
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Important headers to highlight (in order of relevance)
|
|
@@ -164,14 +166,7 @@ async function handleGetEmailHeaders(args) {
|
|
|
164
166
|
const prefix = buildMailboxPrefix(args.sharedMailbox || args.email || null);
|
|
165
167
|
|
|
166
168
|
if (!emailId) {
|
|
167
|
-
return
|
|
168
|
-
content: [
|
|
169
|
-
{
|
|
170
|
-
type: 'text',
|
|
171
|
-
text: 'Email ID is required.',
|
|
172
|
-
},
|
|
173
|
-
],
|
|
174
|
-
};
|
|
169
|
+
return toolError('Email ID is required.');
|
|
175
170
|
}
|
|
176
171
|
|
|
177
172
|
try {
|
|
@@ -206,14 +201,7 @@ async function handleGetEmailHeaders(args) {
|
|
|
206
201
|
);
|
|
207
202
|
|
|
208
203
|
if (!email) {
|
|
209
|
-
return {
|
|
210
|
-
content: [
|
|
211
|
-
{
|
|
212
|
-
type: 'text',
|
|
213
|
-
text: `Email with ID ${emailId} not found.`,
|
|
214
|
-
},
|
|
215
|
-
],
|
|
216
|
-
};
|
|
204
|
+
return toolError(`Email with ID ${emailId} not found.`);
|
|
217
205
|
}
|
|
218
206
|
|
|
219
207
|
const headers = email.internetMessageHeaders || [];
|
|
@@ -296,48 +284,22 @@ async function handleGetEmailHeaders(args) {
|
|
|
296
284
|
},
|
|
297
285
|
};
|
|
298
286
|
} catch (error) {
|
|
299
|
-
|
|
287
|
+
log.debug(`Error getting email headers: ${error.message}`);
|
|
300
288
|
|
|
301
289
|
if (error.message.includes("doesn't belong to the targeted mailbox")) {
|
|
302
|
-
return
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
type: 'text',
|
|
306
|
-
text: `The email ID seems invalid or doesn't belong to your mailbox.`,
|
|
307
|
-
},
|
|
308
|
-
],
|
|
309
|
-
};
|
|
290
|
+
return toolError(
|
|
291
|
+
`The email ID seems invalid or doesn't belong to your mailbox.`
|
|
292
|
+
);
|
|
310
293
|
}
|
|
311
294
|
|
|
312
|
-
return {
|
|
313
|
-
content: [
|
|
314
|
-
{
|
|
315
|
-
type: 'text',
|
|
316
|
-
text: `Failed to get email headers: ${error.message}`,
|
|
317
|
-
},
|
|
318
|
-
],
|
|
319
|
-
};
|
|
295
|
+
return toolError(`Failed to get email headers: ${error.message}`);
|
|
320
296
|
}
|
|
321
297
|
} catch (error) {
|
|
322
298
|
if (error.message === 'Authentication required') {
|
|
323
|
-
return
|
|
324
|
-
content: [
|
|
325
|
-
{
|
|
326
|
-
type: 'text',
|
|
327
|
-
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
328
|
-
},
|
|
329
|
-
],
|
|
330
|
-
};
|
|
299
|
+
return authRequiredError();
|
|
331
300
|
}
|
|
332
301
|
|
|
333
|
-
return {
|
|
334
|
-
content: [
|
|
335
|
-
{
|
|
336
|
-
type: 'text',
|
|
337
|
-
text: `Error accessing email: ${error.message}`,
|
|
338
|
-
},
|
|
339
|
-
],
|
|
340
|
-
};
|
|
302
|
+
return toolError(`Error accessing email: ${error.message}`);
|
|
341
303
|
}
|
|
342
304
|
}
|
|
343
305
|
|