@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/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,59 @@ 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 it ends in a
|
|
71
|
+
// separator or one exists 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 && /[\\/]$/.test(args.savePath)) {
|
|
81
|
+
// A trailing separator names a directory even when it doesn't exist
|
|
82
|
+
// yet; resolving the path drops it, so decide here (#301).
|
|
83
|
+
targetDir = confineOutputPath(args.savePath);
|
|
84
|
+
} else if (args.savePath) {
|
|
85
|
+
const target = confineOutputTarget(args.savePath);
|
|
86
|
+
const resolved = target.path;
|
|
87
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
88
|
+
targetDir = resolved;
|
|
89
|
+
} else {
|
|
90
|
+
explicitFile = resolved;
|
|
91
|
+
requestedFile = target.requested;
|
|
92
|
+
explicitBase = target.base;
|
|
93
|
+
// Refuse early, before fetching; writeExplicitFile checks again.
|
|
94
|
+
if (!overwrite && pathEntryExists(explicitFile)) {
|
|
95
|
+
throw fileExistsError(requestedFile);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
targetDir = confineOutputPath(os.tmpdir());
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return outputPathError(error);
|
|
64
103
|
}
|
|
65
104
|
|
|
66
105
|
try {
|
|
@@ -77,14 +116,7 @@ async function handleExportEmail(args) {
|
|
|
77
116
|
);
|
|
78
117
|
|
|
79
118
|
if (!email) {
|
|
80
|
-
return {
|
|
81
|
-
content: [
|
|
82
|
-
{
|
|
83
|
-
type: 'text',
|
|
84
|
-
text: `Email with ID ${emailId} not found.`,
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
};
|
|
119
|
+
return toolError(`Email with ID ${emailId} not found.`);
|
|
88
120
|
}
|
|
89
121
|
|
|
90
122
|
// Generate filename based on email metadata. The time matters: a
|
|
@@ -98,16 +130,6 @@ async function handleExportEmail(args) {
|
|
|
98
130
|
// Paths claimed while writing this message (main file + attachments).
|
|
99
131
|
const claimedPaths = new Set();
|
|
100
132
|
|
|
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
133
|
// Export based on format
|
|
112
134
|
let content;
|
|
113
135
|
let attachmentsSaved = [];
|
|
@@ -130,34 +152,35 @@ async function handleExportEmail(args) {
|
|
|
130
152
|
} else if (format === 'mbox' || format === 'html') {
|
|
131
153
|
// F-26: clarify that mbox/html are conversation-only formats so
|
|
132
154
|
// 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
|
-
};
|
|
155
|
+
return toolError(
|
|
156
|
+
`Format '${format}' is only supported for target=conversation. For target=message use one of: ${Object.values(EXPORT_FORMATS).join(', ')}.`
|
|
157
|
+
);
|
|
141
158
|
} 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
|
-
};
|
|
159
|
+
return toolError(
|
|
160
|
+
`Unknown format: ${format}. Supported for target=message: ${Object.values(EXPORT_FORMATS).join(', ')}.`
|
|
161
|
+
);
|
|
150
162
|
}
|
|
151
163
|
|
|
152
164
|
// Save main file. Auto-create the directory so callers don't have to
|
|
153
|
-
// pre-mkdir.
|
|
165
|
+
// pre-mkdir. An explicit file is created exclusively and replaces an
|
|
166
|
+
// existing file only with overwrite: true. A directory means we choose
|
|
167
|
+
// the name, so the write is exclusive (`wx`): it never clobbers an
|
|
168
|
+
// existing file and never follows a planted symlink.
|
|
154
169
|
let finalPath;
|
|
170
|
+
let replaced = false;
|
|
155
171
|
if (explicitFile) {
|
|
156
|
-
finalPath =
|
|
157
|
-
|
|
158
|
-
|
|
172
|
+
({ path: finalPath, replaced } = writeExplicitFile(
|
|
173
|
+
explicitFile,
|
|
174
|
+
content,
|
|
175
|
+
{
|
|
176
|
+
overwrite,
|
|
177
|
+
encoding: 'utf8',
|
|
178
|
+
displayPath: requestedFile,
|
|
179
|
+
base: explicitBase,
|
|
180
|
+
}
|
|
181
|
+
));
|
|
159
182
|
} else {
|
|
160
|
-
|
|
183
|
+
ensureOutputDir(targetDir);
|
|
161
184
|
finalPath = writeClaimedFile(
|
|
162
185
|
targetDir,
|
|
163
186
|
defaultBase,
|
|
@@ -184,6 +207,7 @@ async function handleExportEmail(args) {
|
|
|
184
207
|
resultText += `| Property | Value |\n`;
|
|
185
208
|
resultText += `|----------|-------|\n`;
|
|
186
209
|
resultText += `| File | \`${finalPath}\` |\n`;
|
|
210
|
+
if (replaced) resultText += `| Replaced | yes (existing file) |\n`;
|
|
187
211
|
resultText += `| Format | ${format.toUpperCase()} |\n`;
|
|
188
212
|
resultText += `| Size | ${content.length.toLocaleString()} bytes |\n`;
|
|
189
213
|
resultText += `| Subject | ${email.subject} |\n`;
|
|
@@ -206,6 +230,7 @@ async function handleExportEmail(args) {
|
|
|
206
230
|
],
|
|
207
231
|
_meta: {
|
|
208
232
|
filePath: finalPath,
|
|
233
|
+
replaced,
|
|
209
234
|
format: format,
|
|
210
235
|
sizeBytes: content.length,
|
|
211
236
|
attachmentsSaved: attachmentsSaved.length,
|
|
@@ -213,28 +238,27 @@ async function handleExportEmail(args) {
|
|
|
213
238
|
},
|
|
214
239
|
};
|
|
215
240
|
} catch (error) {
|
|
241
|
+
if (error instanceof OutputPathError) {
|
|
242
|
+
return outputPathError(error);
|
|
243
|
+
}
|
|
216
244
|
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
|
-
};
|
|
245
|
+
return authRequiredError();
|
|
225
246
|
}
|
|
226
247
|
|
|
227
|
-
return {
|
|
228
|
-
content: [
|
|
229
|
-
{
|
|
230
|
-
type: 'text',
|
|
231
|
-
text: `Export failed: ${error.message}`,
|
|
232
|
-
},
|
|
233
|
-
],
|
|
234
|
-
};
|
|
248
|
+
return toolError(`Export failed: ${error.message}`);
|
|
235
249
|
}
|
|
236
250
|
}
|
|
237
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Tool error for a refused output path; rethrows anything else.
|
|
254
|
+
* @param {Error} error
|
|
255
|
+
* @returns {object} MCP error response
|
|
256
|
+
*/
|
|
257
|
+
function outputPathError(error) {
|
|
258
|
+
if (!(error instanceof OutputPathError)) throw error;
|
|
259
|
+
return toolError(error.message, { nextStep: error.nextStep });
|
|
260
|
+
}
|
|
261
|
+
|
|
238
262
|
/**
|
|
239
263
|
* Batch export emails handler
|
|
240
264
|
* @param {object} args - Tool arguments
|
|
@@ -255,28 +279,25 @@ async function handleBatchExportEmails(args) {
|
|
|
255
279
|
searchQuery.subject = args.query;
|
|
256
280
|
}
|
|
257
281
|
const format = (args.format || EXPORT_FORMATS.MARKDOWN).toLowerCase();
|
|
258
|
-
const outputDir = args.outputDir;
|
|
259
282
|
const includeAttachments = args.includeAttachments === true; // Default false for batch
|
|
260
283
|
// Scope the whole batch (search + per-message fetch + attachments) to a
|
|
261
284
|
// shared/delegated mailbox when supplied.
|
|
262
285
|
const mailbox = args.sharedMailbox || args.email || null;
|
|
263
286
|
const prefix = buildMailboxPrefix(mailbox);
|
|
264
287
|
|
|
265
|
-
if (!outputDir) {
|
|
266
|
-
return
|
|
267
|
-
content: [
|
|
268
|
-
{
|
|
269
|
-
type: 'text',
|
|
270
|
-
text: 'Output directory is required.',
|
|
271
|
-
},
|
|
272
|
-
],
|
|
273
|
-
};
|
|
288
|
+
if (!args.outputDir) {
|
|
289
|
+
return toolError('Output directory is required.');
|
|
274
290
|
}
|
|
275
291
|
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
292
|
+
// Resolve and check the directory before creating it; write only to the
|
|
293
|
+
// resolved path.
|
|
294
|
+
let outputDir;
|
|
295
|
+
try {
|
|
296
|
+
outputDir = confineOutputPath(args.outputDir);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
return outputPathError(error);
|
|
279
299
|
}
|
|
300
|
+
ensureOutputDir(outputDir);
|
|
280
301
|
|
|
281
302
|
try {
|
|
282
303
|
const accessToken = await ensureAuthenticated();
|
|
@@ -290,24 +311,29 @@ async function handleBatchExportEmails(args) {
|
|
|
290
311
|
mailbox
|
|
291
312
|
);
|
|
292
313
|
idsToExport = searchResults.map((e) => e.id);
|
|
314
|
+
// An empty match is a result, not an error.
|
|
315
|
+
if (idsToExport.length === 0) {
|
|
316
|
+
return {
|
|
317
|
+
content: [
|
|
318
|
+
{
|
|
319
|
+
type: 'text',
|
|
320
|
+
text: 'No emails matched the search query; nothing was exported.',
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
293
325
|
}
|
|
294
326
|
|
|
295
327
|
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
|
-
};
|
|
328
|
+
return toolError('No emails to export. Provide emailIds or searchQuery.');
|
|
304
329
|
}
|
|
305
330
|
|
|
306
|
-
// Limit batch size (per plan: max 100)
|
|
331
|
+
// Limit batch size (per plan: max 100), and say so in the result (#279)
|
|
307
332
|
const maxBatch = 100;
|
|
333
|
+
const limitNote = batchLimitNote(idsToExport.length, emailIds, searchQuery);
|
|
308
334
|
if (idsToExport.length > maxBatch) {
|
|
309
335
|
idsToExport = idsToExport.slice(0, maxBatch);
|
|
310
|
-
|
|
336
|
+
log.debug(`Batch export limited to ${maxBatch} emails`);
|
|
311
337
|
}
|
|
312
338
|
|
|
313
339
|
// CSV batch export: aggregate all emails into a single CSV file
|
|
@@ -333,7 +359,7 @@ async function handleBatchExportEmails(args) {
|
|
|
333
359
|
|
|
334
360
|
const csvContent = formatEmailsAsCSV(emails);
|
|
335
361
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
336
|
-
|
|
362
|
+
ensureOutputDir(outputDir);
|
|
337
363
|
const csvPath = writeClaimedFile(
|
|
338
364
|
outputDir,
|
|
339
365
|
`batch_export_${timestamp}`,
|
|
@@ -353,6 +379,7 @@ async function handleBatchExportEmails(args) {
|
|
|
353
379
|
resultText += `| Output File | \`${csvPath}\` |\n`;
|
|
354
380
|
resultText += `| Format | CSV |\n`;
|
|
355
381
|
resultText += `| Total Size | ${(totalBytes / 1024).toFixed(1)} KB |\n`;
|
|
382
|
+
resultText += limitNote;
|
|
356
383
|
|
|
357
384
|
if (failed.length > 0) {
|
|
358
385
|
resultText += `\n### Failed Exports\n\n`;
|
|
@@ -407,6 +434,7 @@ async function handleBatchExportEmails(args) {
|
|
|
407
434
|
0
|
|
408
435
|
);
|
|
409
436
|
resultText += `| Total Size | ${(totalBytes / 1024).toFixed(1)} KB |\n`;
|
|
437
|
+
resultText += limitNote;
|
|
410
438
|
|
|
411
439
|
// Requested id -> written path, so a caller can reconcile without
|
|
412
440
|
// listing the directory. A batch that silently lost messages to
|
|
@@ -452,25 +480,32 @@ async function handleBatchExportEmails(args) {
|
|
|
452
480
|
};
|
|
453
481
|
} catch (error) {
|
|
454
482
|
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
|
-
};
|
|
483
|
+
return authRequiredError();
|
|
463
484
|
}
|
|
464
485
|
|
|
465
|
-
return {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
486
|
+
return toolError(`Batch export failed: ${error.message}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Says when a batch export was cut short, and how to get the rest (#279).
|
|
492
|
+
* target=messages exports at most 100 messages per call, and a search stops
|
|
493
|
+
* at searchQuery.maxResults (default 25, max 100).
|
|
494
|
+
* @param {number} found - IDs given or matched before the cap
|
|
495
|
+
* @param {string[]} emailIds - IDs the caller passed (empty for a search)
|
|
496
|
+
* @param {object} searchQuery - The search used when emailIds is empty
|
|
497
|
+
* @returns {string} - Markdown note, or '' when nothing was left out
|
|
498
|
+
*/
|
|
499
|
+
function batchLimitNote(found, emailIds, searchQuery) {
|
|
500
|
+
if (emailIds.length > 0) {
|
|
501
|
+
if (found <= 100) return '';
|
|
502
|
+
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
503
|
}
|
|
504
|
+
const searchLimit = Math.min(searchQuery.maxResults || 25, 100);
|
|
505
|
+
if (found < searchLimit) return '';
|
|
506
|
+
const raise =
|
|
507
|
+
searchLimit < 100 ? 'raise `searchQuery.maxResults` (up to 100) or ' : '';
|
|
508
|
+
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
509
|
}
|
|
475
510
|
|
|
476
511
|
/**
|
|
@@ -707,7 +742,7 @@ async function saveAttachments(
|
|
|
707
742
|
}
|
|
708
743
|
}
|
|
709
744
|
} catch (error) {
|
|
710
|
-
|
|
745
|
+
log.debug(`Failed to save attachments: ${error.message}`);
|
|
711
746
|
}
|
|
712
747
|
|
|
713
748
|
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
|
|