@littlebearapps/outlook-assistant 3.7.2 → 3.7.4
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 +35 -18
- package/advanced/index.js +26 -3
- package/auth/tools.js +54 -1
- package/calendar/index.js +27 -1
- package/categories/index.js +67 -28
- package/contacts/index.js +118 -27
- package/email/attachments.js +13 -3
- package/email/conversations.js +4 -7
- package/email/delta.js +20 -6
- package/email/export.js +26 -3
- package/email/index.js +38 -4
- package/email/list.js +6 -1
- package/email/mail-tips.js +26 -2
- package/email/search.js +82 -25
- package/folder/create.js +6 -1
- package/folder/index.js +10 -1
- package/index.js +33 -0
- package/llms.txt +8 -2
- package/package.json +1 -1
- package/rules/create.js +5 -1
- package/rules/index.js +19 -1
- package/rules/update.js +11 -3
- package/settings/index.js +70 -2
- package/utils/response-formatter.js +27 -0
- package/utils/schema-coerce.js +248 -0
package/contacts/index.js
CHANGED
|
@@ -76,13 +76,10 @@ function formatContact(contact, verbosity = 'standard') {
|
|
|
76
76
|
lines.push(`**Phone**: ${phones.join(' | ')}`);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
.join(' at ');
|
|
84
|
-
lines.push(`**Company**: ${company}`);
|
|
85
|
-
}
|
|
79
|
+
// Job title and company info — F-40: previously squashed into a
|
|
80
|
+
// single 'Company' label which mislabeled jobTitle-only contacts.
|
|
81
|
+
if (contact.jobTitle) lines.push(`**Job Title**: ${contact.jobTitle}`);
|
|
82
|
+
if (contact.companyName) lines.push(`**Company**: ${contact.companyName}`);
|
|
86
83
|
}
|
|
87
84
|
|
|
88
85
|
// Full verbosity extras
|
|
@@ -130,6 +127,8 @@ async function handleListContacts(args) {
|
|
|
130
127
|
const verbosity = args.outputVerbosity || 'standard';
|
|
131
128
|
const folder = args.folder || null; // null = default contacts folder
|
|
132
129
|
|
|
130
|
+
const skip = args.skip || 0;
|
|
131
|
+
|
|
133
132
|
try {
|
|
134
133
|
const accessToken = await ensureAuthenticated();
|
|
135
134
|
|
|
@@ -142,7 +141,9 @@ async function handleListContacts(args) {
|
|
|
142
141
|
$select: fields.join(','),
|
|
143
142
|
$top: count,
|
|
144
143
|
$orderby: 'displayName',
|
|
144
|
+
$count: 'true', // Surface true total so callers know if more pages exist
|
|
145
145
|
};
|
|
146
|
+
if (skip > 0) queryParams.$skip = skip;
|
|
146
147
|
|
|
147
148
|
const response = await callGraphAPI(
|
|
148
149
|
accessToken,
|
|
@@ -152,10 +153,29 @@ async function handleListContacts(args) {
|
|
|
152
153
|
queryParams
|
|
153
154
|
);
|
|
154
155
|
const contacts = response.value || [];
|
|
156
|
+
const totalAvailable = response['@odata.count'];
|
|
157
|
+
const hasMore = Boolean(response['@odata.nextLink']);
|
|
155
158
|
|
|
156
159
|
const output = [];
|
|
157
160
|
output.push(`# Contacts\n`);
|
|
158
|
-
|
|
161
|
+
if (typeof totalAvailable === 'number') {
|
|
162
|
+
output.push(
|
|
163
|
+
`**Showing**: ${contacts.length} of ${totalAvailable}${skip > 0 ? ` (offset ${skip})` : ''}`
|
|
164
|
+
);
|
|
165
|
+
} else {
|
|
166
|
+
output.push(`**Showing**: ${contacts.length}`);
|
|
167
|
+
}
|
|
168
|
+
// F-22: surface pagination cue when more results exist so callers
|
|
169
|
+
// know to ask for the next page instead of assuming "Total: 50"
|
|
170
|
+
// is the entire address book.
|
|
171
|
+
if (
|
|
172
|
+
hasMore ||
|
|
173
|
+
(totalAvailable && contacts.length + skip < totalAvailable)
|
|
174
|
+
) {
|
|
175
|
+
output.push(
|
|
176
|
+
`**More available**: pass \`skip: ${skip + contacts.length}\` to fetch the next page (or \`count\` to raise the page size up to 100).`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
159
179
|
output.push('');
|
|
160
180
|
|
|
161
181
|
contacts.forEach((contact) => {
|
|
@@ -164,7 +184,13 @@ async function handleListContacts(args) {
|
|
|
164
184
|
|
|
165
185
|
return {
|
|
166
186
|
content: [{ type: 'text', text: output.join('\n') }],
|
|
167
|
-
_meta: {
|
|
187
|
+
_meta: {
|
|
188
|
+
count: contacts.length,
|
|
189
|
+
...(typeof totalAvailable === 'number' && {
|
|
190
|
+
totalAvailable,
|
|
191
|
+
}),
|
|
192
|
+
hasMore,
|
|
193
|
+
},
|
|
168
194
|
};
|
|
169
195
|
} catch (error) {
|
|
170
196
|
if (error.message === 'Authentication required') {
|
|
@@ -340,13 +366,32 @@ async function handleGetContact(args) {
|
|
|
340
366
|
* Create contact handler
|
|
341
367
|
*/
|
|
342
368
|
async function handleCreateContact(args) {
|
|
343
|
-
const {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
369
|
+
const {
|
|
370
|
+
displayName,
|
|
371
|
+
firstName,
|
|
372
|
+
lastName,
|
|
373
|
+
email,
|
|
374
|
+
emails,
|
|
375
|
+
mobilePhone,
|
|
376
|
+
companyName,
|
|
377
|
+
jobTitle,
|
|
378
|
+
notes,
|
|
379
|
+
} = args;
|
|
380
|
+
|
|
381
|
+
// F-39: derive displayName from firstName/lastName if not provided.
|
|
382
|
+
const resolvedDisplayName =
|
|
383
|
+
displayName ||
|
|
384
|
+
[firstName, lastName].filter(Boolean).join(' ') ||
|
|
385
|
+
email ||
|
|
386
|
+
(Array.isArray(emails) && emails[0]);
|
|
387
|
+
|
|
388
|
+
if (!resolvedDisplayName && !email && !(emails && emails.length > 0)) {
|
|
347
389
|
return {
|
|
348
390
|
content: [
|
|
349
|
-
{
|
|
391
|
+
{
|
|
392
|
+
type: 'text',
|
|
393
|
+
text: 'At least displayName, firstName/lastName, email, or emails is required.',
|
|
394
|
+
},
|
|
350
395
|
],
|
|
351
396
|
};
|
|
352
397
|
}
|
|
@@ -356,24 +401,39 @@ async function handleCreateContact(args) {
|
|
|
356
401
|
|
|
357
402
|
const contactData = {};
|
|
358
403
|
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
404
|
+
if (resolvedDisplayName) contactData.displayName = resolvedDisplayName;
|
|
405
|
+
|
|
406
|
+
// F-39: prefer explicit givenName/surname when supplied; otherwise
|
|
407
|
+
// derive from displayName if it's a multi-word string.
|
|
408
|
+
if (firstName) contactData.givenName = firstName;
|
|
409
|
+
if (lastName) contactData.surname = lastName;
|
|
410
|
+
if (
|
|
411
|
+
!contactData.givenName &&
|
|
412
|
+
!contactData.surname &&
|
|
413
|
+
displayName &&
|
|
414
|
+
displayName.includes(' ')
|
|
415
|
+
) {
|
|
416
|
+
const parts = displayName.split(' ');
|
|
417
|
+
contactData.givenName = parts[0];
|
|
418
|
+
contactData.surname = parts.slice(1).join(' ');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Email: accept either `email` (single) or `emails` (array).
|
|
422
|
+
const allEmails = [];
|
|
423
|
+
if (email) allEmails.push(email);
|
|
424
|
+
if (Array.isArray(emails)) allEmails.push(...emails);
|
|
425
|
+
if (allEmails.length > 0) {
|
|
426
|
+
contactData.emailAddresses = allEmails.map((addr) => ({
|
|
427
|
+
address: addr,
|
|
428
|
+
name: resolvedDisplayName || addr,
|
|
429
|
+
}));
|
|
364
430
|
}
|
|
431
|
+
|
|
365
432
|
if (mobilePhone) contactData.mobilePhone = mobilePhone;
|
|
366
433
|
if (companyName) contactData.companyName = companyName;
|
|
367
434
|
if (jobTitle) contactData.jobTitle = jobTitle;
|
|
368
435
|
if (notes) contactData.personalNotes = notes;
|
|
369
436
|
|
|
370
|
-
// Parse name into given/surname if provided
|
|
371
|
-
if (displayName && displayName.includes(' ')) {
|
|
372
|
-
const parts = displayName.split(' ');
|
|
373
|
-
contactData.givenName = parts[0];
|
|
374
|
-
contactData.surname = parts.slice(1).join(' ');
|
|
375
|
-
}
|
|
376
|
-
|
|
377
437
|
const contact = await callGraphAPI(
|
|
378
438
|
accessToken,
|
|
379
439
|
'POST',
|
|
@@ -640,6 +700,11 @@ const contactsTools = [
|
|
|
640
700
|
description:
|
|
641
701
|
'Number of results (action=list default: 50, action=search default: 25)',
|
|
642
702
|
},
|
|
703
|
+
skip: {
|
|
704
|
+
type: 'integer',
|
|
705
|
+
description:
|
|
706
|
+
'Pagination offset for action=list (default: 0). Use the value suggested by the previous page response.',
|
|
707
|
+
},
|
|
643
708
|
folder: {
|
|
644
709
|
type: 'string',
|
|
645
710
|
description: 'Contact folder ID (action=list)',
|
|
@@ -666,10 +731,26 @@ const contactsTools = [
|
|
|
666
731
|
type: 'string',
|
|
667
732
|
description: 'Full name (action=create/update)',
|
|
668
733
|
},
|
|
734
|
+
firstName: {
|
|
735
|
+
type: 'string',
|
|
736
|
+
description:
|
|
737
|
+
'Given name (action=create/update). Maps to Graph `givenName`. If displayName not provided, will be combined with lastName.',
|
|
738
|
+
},
|
|
739
|
+
lastName: {
|
|
740
|
+
type: 'string',
|
|
741
|
+
description:
|
|
742
|
+
'Surname (action=create/update). Maps to Graph `surname`.',
|
|
743
|
+
},
|
|
669
744
|
email: {
|
|
670
745
|
type: 'string',
|
|
671
746
|
description: 'Primary email address (action=create/update)',
|
|
672
747
|
},
|
|
748
|
+
emails: {
|
|
749
|
+
type: 'array',
|
|
750
|
+
items: { type: 'string' },
|
|
751
|
+
description:
|
|
752
|
+
'Multiple email addresses (action=create/update). First entry is primary.',
|
|
753
|
+
},
|
|
673
754
|
mobilePhone: {
|
|
674
755
|
type: 'string',
|
|
675
756
|
description: 'Mobile phone number (action=create/update)',
|
|
@@ -687,6 +768,7 @@ const contactsTools = [
|
|
|
687
768
|
description: 'Personal notes (action=create/update)',
|
|
688
769
|
},
|
|
689
770
|
},
|
|
771
|
+
additionalProperties: false,
|
|
690
772
|
required: [],
|
|
691
773
|
},
|
|
692
774
|
handler: async (args) => {
|
|
@@ -703,8 +785,16 @@ const contactsTools = [
|
|
|
703
785
|
case 'delete':
|
|
704
786
|
return handleDeleteContact(args);
|
|
705
787
|
case 'list':
|
|
706
|
-
default:
|
|
707
788
|
return handleListContacts(args);
|
|
789
|
+
default:
|
|
790
|
+
return {
|
|
791
|
+
content: [
|
|
792
|
+
{
|
|
793
|
+
type: 'text',
|
|
794
|
+
text: `Unknown action '${action}'. Valid actions: list, search, get, create, update, delete.`,
|
|
795
|
+
},
|
|
796
|
+
],
|
|
797
|
+
};
|
|
708
798
|
}
|
|
709
799
|
},
|
|
710
800
|
},
|
|
@@ -729,6 +819,7 @@ const contactsTools = [
|
|
|
729
819
|
description: 'Maximum results to return (default: 25, max: 50)',
|
|
730
820
|
},
|
|
731
821
|
},
|
|
822
|
+
additionalProperties: false,
|
|
732
823
|
required: ['query'],
|
|
733
824
|
},
|
|
734
825
|
handler: handleSearchPeople,
|
package/email/attachments.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
const _https = require('https'); // Reserved for future use
|
|
6
6
|
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
7
8
|
const path = require('path');
|
|
8
9
|
const _config = require('../config'); // Reserved for future use
|
|
9
10
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
@@ -110,7 +111,12 @@ async function handleListAttachments(args) {
|
|
|
110
111
|
* @returns {object} - MCP response with download result
|
|
111
112
|
*/
|
|
112
113
|
async function handleDownloadAttachment(args) {
|
|
113
|
-
|
|
114
|
+
// F-19: accept both `outputDir` (canonical) and `savePath` (legacy
|
|
115
|
+
// alias). Previously the silent-ignore-unknown-param behaviour
|
|
116
|
+
// dropped `outputDir` and fell through to cwd, polluting the source
|
|
117
|
+
// tree with downloaded files.
|
|
118
|
+
const { messageId, attachmentId } = args;
|
|
119
|
+
const savePath = args.outputDir || args.savePath;
|
|
114
120
|
|
|
115
121
|
if (!messageId || !attachmentId) {
|
|
116
122
|
return {
|
|
@@ -167,8 +173,12 @@ async function handleDownloadAttachment(args) {
|
|
|
167
173
|
};
|
|
168
174
|
}
|
|
169
175
|
|
|
170
|
-
// Determine save location
|
|
171
|
-
|
|
176
|
+
// Determine save location. F-19: default to os.tmpdir() instead
|
|
177
|
+
// of cwd so attachments don't silently land in the source tree
|
|
178
|
+
// when the caller forgets to pass outputDir. Auto-create the
|
|
179
|
+
// target directory.
|
|
180
|
+
const outputDir = savePath || os.tmpdir();
|
|
181
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
172
182
|
const outputPath = path.join(outputDir, filename);
|
|
173
183
|
|
|
174
184
|
// Decode base64 and save to file
|
package/email/conversations.js
CHANGED
|
@@ -372,7 +372,10 @@ async function handleGetConversation(args) {
|
|
|
372
372
|
async function handleExportConversation(args) {
|
|
373
373
|
const conversationId = args.conversationId;
|
|
374
374
|
const format = (args.format || 'markdown').toLowerCase();
|
|
375
|
-
|
|
375
|
+
// F-29: default outputDir to os.tmpdir() to match target=message
|
|
376
|
+
// behaviour. Previously conversation export rejected calls without
|
|
377
|
+
// outputDir, inconsistent with the other export targets.
|
|
378
|
+
const outputDir = args.outputDir || require('os').tmpdir();
|
|
376
379
|
const _includeAttachments = args.includeAttachments !== false;
|
|
377
380
|
const order = args.order || 'chronological';
|
|
378
381
|
|
|
@@ -382,12 +385,6 @@ async function handleExportConversation(args) {
|
|
|
382
385
|
};
|
|
383
386
|
}
|
|
384
387
|
|
|
385
|
-
if (!outputDir) {
|
|
386
|
-
return {
|
|
387
|
-
content: [{ type: 'text', text: 'Output directory is required.' }],
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
|
|
391
388
|
const validFormats = ['eml', 'mbox', 'markdown', 'json', 'html', 'csv'];
|
|
392
389
|
if (!validFormats.includes(format)) {
|
|
393
390
|
return {
|
package/email/delta.js
CHANGED
|
@@ -90,6 +90,11 @@ async function handleListEmailsDelta(args) {
|
|
|
90
90
|
const isInitialSync = !deltaToken;
|
|
91
91
|
const hasMoreChanges = Boolean(nextLink);
|
|
92
92
|
const newDeltaToken = deltaLink || nextLink;
|
|
93
|
+
// F-15: nextLink is a continuation token (more pages of the same
|
|
94
|
+
// sync), not a delta token. The real delta token only emits once
|
|
95
|
+
// the initial sync finishes paging. Distinguish them in output so
|
|
96
|
+
// callers know what they're storing.
|
|
97
|
+
const tokenIsContinuation = !deltaLink && Boolean(nextLink);
|
|
93
98
|
|
|
94
99
|
// Format output based on verbosity
|
|
95
100
|
let resultText;
|
|
@@ -101,7 +106,10 @@ async function handleListEmailsDelta(args) {
|
|
|
101
106
|
resultText += `| Type | ${isInitialSync ? 'Initial' : 'Incremental'} |\n`;
|
|
102
107
|
resultText += `| More | ${hasMoreChanges ? 'Yes' : 'No'} |\n`;
|
|
103
108
|
if (newDeltaToken) {
|
|
104
|
-
|
|
109
|
+
const label = tokenIsContinuation
|
|
110
|
+
? 'Continuation Token (more pages — call again to keep paging)'
|
|
111
|
+
: 'Delta Token (save for next sync call)';
|
|
112
|
+
resultText += `\n**${label}**:\n\`\`\`\n${newDeltaToken}\n\`\`\`\n`;
|
|
105
113
|
}
|
|
106
114
|
} else {
|
|
107
115
|
resultText = `## Delta Sync ${isInitialSync ? '(Initial)' : '(Incremental)'}\n\n`;
|
|
@@ -142,14 +150,19 @@ async function handleListEmailsDelta(args) {
|
|
|
142
150
|
|
|
143
151
|
// Pagination info
|
|
144
152
|
if (hasMoreChanges) {
|
|
145
|
-
resultText += `\n### More
|
|
146
|
-
resultText += `
|
|
153
|
+
resultText += `\n### More Pages Available\n`;
|
|
154
|
+
resultText += `This page returned a continuation token. Call \`search-emails deltaMode=true deltaToken=<token>\` again to fetch the next page. The real delta token only emits once paging completes.\n`;
|
|
147
155
|
}
|
|
148
156
|
|
|
149
|
-
//
|
|
157
|
+
// Token (delta or continuation)
|
|
150
158
|
if (newDeltaToken) {
|
|
151
|
-
|
|
152
|
-
|
|
159
|
+
if (tokenIsContinuation) {
|
|
160
|
+
resultText += `\n### Continuation Token\n`;
|
|
161
|
+
resultText += `**More pages remain. Pass this back to keep paging:**\n\`\`\`\n${newDeltaToken}\n\`\`\`\n`;
|
|
162
|
+
} else {
|
|
163
|
+
resultText += `\n### Delta Token\n`;
|
|
164
|
+
resultText += `**Save this token for next sync call:**\n\`\`\`\n${newDeltaToken}\n\`\`\`\n`;
|
|
165
|
+
}
|
|
153
166
|
}
|
|
154
167
|
}
|
|
155
168
|
|
|
@@ -167,6 +180,7 @@ async function handleListEmailsDelta(args) {
|
|
|
167
180
|
hasMoreChanges: hasMoreChanges,
|
|
168
181
|
changesSummary: changesSummary,
|
|
169
182
|
deltaToken: newDeltaToken,
|
|
183
|
+
tokenType: tokenIsContinuation ? 'continuation' : 'delta',
|
|
170
184
|
},
|
|
171
185
|
};
|
|
172
186
|
} catch (error) {
|
package/email/export.js
CHANGED
|
@@ -37,7 +37,10 @@ const EXPORT_FORMATS = {
|
|
|
37
37
|
async function handleExportEmail(args) {
|
|
38
38
|
const emailId = args.id;
|
|
39
39
|
const format = (args.format || EXPORT_FORMATS.MARKDOWN).toLowerCase();
|
|
40
|
-
|
|
40
|
+
// F-27: accept `outputDir` (canonical) and `savePath` (legacy alias).
|
|
41
|
+
// Previously single-message exports ignored outputDir entirely and
|
|
42
|
+
// hardcoded os.tmpdir(), inconsistent with target=messages.
|
|
43
|
+
const savePath = args.outputDir || args.savePath;
|
|
41
44
|
const includeAttachments = args.includeAttachments !== false;
|
|
42
45
|
|
|
43
46
|
if (!emailId) {
|
|
@@ -116,17 +119,31 @@ async function handleExportEmail(args) {
|
|
|
116
119
|
} else if (format === EXPORT_FORMATS.CSV) {
|
|
117
120
|
// CSV export - email metadata
|
|
118
121
|
content = formatEmailsAsCSV(email);
|
|
122
|
+
} else if (format === 'mbox' || format === 'html') {
|
|
123
|
+
// F-26: clarify that mbox/html are conversation-only formats so
|
|
124
|
+
// callers don't infer the format itself is unsupported.
|
|
125
|
+
return {
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: 'text',
|
|
129
|
+
text: `Format '${format}' is only supported for target=conversation. For target=message use one of: ${Object.values(EXPORT_FORMATS).join(', ')}.`,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
119
133
|
} else {
|
|
120
134
|
return {
|
|
121
135
|
content: [
|
|
122
136
|
{
|
|
123
137
|
type: 'text',
|
|
124
|
-
text: `Unknown format: ${format}. Supported: ${Object.values(EXPORT_FORMATS).join(', ')}
|
|
138
|
+
text: `Unknown format: ${format}. Supported for target=message: ${Object.values(EXPORT_FORMATS).join(', ')}.`,
|
|
125
139
|
},
|
|
126
140
|
],
|
|
127
141
|
};
|
|
128
142
|
}
|
|
129
143
|
|
|
144
|
+
// Auto-create the parent directory so callers don't have to pre-mkdir.
|
|
145
|
+
fs.mkdirSync(path.dirname(finalPath), { recursive: true });
|
|
146
|
+
|
|
130
147
|
// Save main file
|
|
131
148
|
fs.writeFileSync(finalPath, content, 'utf8');
|
|
132
149
|
|
|
@@ -207,7 +224,13 @@ async function handleExportEmail(args) {
|
|
|
207
224
|
*/
|
|
208
225
|
async function handleBatchExportEmails(args) {
|
|
209
226
|
const emailIds = args.emailIds || [];
|
|
210
|
-
|
|
227
|
+
// F-28: accept `query` as a top-level string alias for
|
|
228
|
+
// `searchQuery: { subject }`. Lets callers use the same `query`
|
|
229
|
+
// word they already know from search-emails.
|
|
230
|
+
const searchQuery = { ...(args.searchQuery || {}) };
|
|
231
|
+
if (args.query && !searchQuery.subject) {
|
|
232
|
+
searchQuery.subject = args.query;
|
|
233
|
+
}
|
|
211
234
|
const format = (args.format || EXPORT_FORMATS.MARKDOWN).toLowerCase();
|
|
212
235
|
const outputDir = args.outputDir;
|
|
213
236
|
const includeAttachments = args.includeAttachments === true; // Default false for batch
|
package/email/index.js
CHANGED
|
@@ -137,6 +137,7 @@ const emailTools = [
|
|
|
137
137
|
'Include email headers for each message (conversationId only)',
|
|
138
138
|
},
|
|
139
139
|
},
|
|
140
|
+
additionalProperties: false,
|
|
140
141
|
required: [],
|
|
141
142
|
},
|
|
142
143
|
handler: async (args) => {
|
|
@@ -223,6 +224,7 @@ const emailTools = [
|
|
|
223
224
|
'Return raw JSON instead of Markdown (headersMode only, default: false)',
|
|
224
225
|
},
|
|
225
226
|
},
|
|
227
|
+
additionalProperties: false,
|
|
226
228
|
required: ['id'],
|
|
227
229
|
},
|
|
228
230
|
handler: async (args) => {
|
|
@@ -286,6 +288,7 @@ const emailTools = [
|
|
|
286
288
|
'Check recipients for out-of-office, mailbox full, delivery restrictions before sending (default: false). Combine with dryRun=true for pre-send review.',
|
|
287
289
|
},
|
|
288
290
|
},
|
|
291
|
+
additionalProperties: false,
|
|
289
292
|
required: ['to', 'subject', 'body'],
|
|
290
293
|
},
|
|
291
294
|
handler: handleSendEmail,
|
|
@@ -364,6 +367,7 @@ const emailTools = [
|
|
|
364
367
|
'Check recipients for out-of-office, delivery restrictions before saving (action=create, default: false)',
|
|
365
368
|
},
|
|
366
369
|
},
|
|
370
|
+
additionalProperties: false,
|
|
367
371
|
required: ['action'],
|
|
368
372
|
},
|
|
369
373
|
handler: handleDraft,
|
|
@@ -408,6 +412,7 @@ const emailTools = [
|
|
|
408
412
|
description: 'Start date/time for follow-up, ISO 8601 (action=flag)',
|
|
409
413
|
},
|
|
410
414
|
},
|
|
415
|
+
additionalProperties: false,
|
|
411
416
|
required: ['action'],
|
|
412
417
|
},
|
|
413
418
|
handler: async (args) => {
|
|
@@ -473,12 +478,18 @@ const emailTools = [
|
|
|
473
478
|
type: 'string',
|
|
474
479
|
description: 'Attachment ID (action=view/download, required)',
|
|
475
480
|
},
|
|
481
|
+
outputDir: {
|
|
482
|
+
type: 'string',
|
|
483
|
+
description:
|
|
484
|
+
'Directory to save file (action=download, default: system tmpdir). Auto-created if missing.',
|
|
485
|
+
},
|
|
476
486
|
savePath: {
|
|
477
487
|
type: 'string',
|
|
478
488
|
description:
|
|
479
|
-
'
|
|
489
|
+
'DEPRECATED alias for `outputDir`. Will be removed in v3.8.0.',
|
|
480
490
|
},
|
|
481
491
|
},
|
|
492
|
+
additionalProperties: false,
|
|
482
493
|
required: ['messageId'],
|
|
483
494
|
},
|
|
484
495
|
handler: async (args) => {
|
|
@@ -489,8 +500,16 @@ const emailTools = [
|
|
|
489
500
|
case 'download':
|
|
490
501
|
return handleDownloadAttachment(args);
|
|
491
502
|
case 'list':
|
|
492
|
-
default:
|
|
493
503
|
return handleListAttachments(args);
|
|
504
|
+
default:
|
|
505
|
+
return {
|
|
506
|
+
content: [
|
|
507
|
+
{
|
|
508
|
+
type: 'text',
|
|
509
|
+
text: `Unknown action '${action}'. Valid actions: list, view, download.`,
|
|
510
|
+
},
|
|
511
|
+
],
|
|
512
|
+
};
|
|
494
513
|
}
|
|
495
514
|
},
|
|
496
515
|
},
|
|
@@ -521,7 +540,7 @@ const emailTools = [
|
|
|
521
540
|
type: 'string',
|
|
522
541
|
enum: ['mime', 'eml', 'markdown', 'json', 'mbox', 'html', 'csv'],
|
|
523
542
|
description:
|
|
524
|
-
'Export format
|
|
543
|
+
'Export format. Valid values vary by target: target=message accepts mime/eml/markdown/json/csv (mbox and html are conversation-only). target=conversation accepts eml/mbox/markdown/json/html/csv. target=messages (batch) accepts markdown/json/csv. mime is an alias for eml (same RFC822 bytes, .eml extension on disk).',
|
|
525
544
|
},
|
|
526
545
|
savePath: {
|
|
527
546
|
type: 'string',
|
|
@@ -551,6 +570,11 @@ const emailTools = [
|
|
|
551
570
|
description:
|
|
552
571
|
'Search query to find emails (target=messages, alternative to emailIds)',
|
|
553
572
|
},
|
|
573
|
+
query: {
|
|
574
|
+
type: 'string',
|
|
575
|
+
description:
|
|
576
|
+
'Free-text search shortcut (target=messages). Equivalent to passing searchQuery: { subject: <query> }. Convenience alias for callers used to search-emails.',
|
|
577
|
+
},
|
|
554
578
|
outputDir: {
|
|
555
579
|
type: 'string',
|
|
556
580
|
description:
|
|
@@ -581,6 +605,7 @@ const emailTools = [
|
|
|
581
605
|
description: 'Max content size in bytes (target=mime, default: 1MB)',
|
|
582
606
|
},
|
|
583
607
|
},
|
|
608
|
+
additionalProperties: false,
|
|
584
609
|
required: [],
|
|
585
610
|
},
|
|
586
611
|
handler: async (args) => {
|
|
@@ -593,8 +618,16 @@ const emailTools = [
|
|
|
593
618
|
case 'mime':
|
|
594
619
|
return handleGetMimeContent(args);
|
|
595
620
|
case 'message':
|
|
596
|
-
default:
|
|
597
621
|
return handleExportEmail(args);
|
|
622
|
+
default:
|
|
623
|
+
return {
|
|
624
|
+
content: [
|
|
625
|
+
{
|
|
626
|
+
type: 'text',
|
|
627
|
+
text: `Unknown export target '${target}'. Valid targets: message, messages, conversation, mime.`,
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
};
|
|
598
631
|
}
|
|
599
632
|
},
|
|
600
633
|
},
|
|
@@ -630,6 +663,7 @@ const emailTools = [
|
|
|
630
663
|
'Comma-separated tip types to request (default: all). Options: automaticReplies, mailboxFullStatus, customMailTip, externalMemberCount, totalMemberCount, maxMessageSize, deliveryRestriction, moderationStatus, recipientScope, recipientSuggestions',
|
|
631
664
|
},
|
|
632
665
|
},
|
|
666
|
+
additionalProperties: false,
|
|
633
667
|
required: ['recipients'],
|
|
634
668
|
},
|
|
635
669
|
handler: handleGetMailTips,
|
package/email/list.js
CHANGED
|
@@ -44,7 +44,12 @@ function getFieldPresetForVerbosity(verbosity) {
|
|
|
44
44
|
*/
|
|
45
45
|
async function handleListEmails(args) {
|
|
46
46
|
const folder = args.folder || 'inbox';
|
|
47
|
-
|
|
47
|
+
// F-17: accept `maxResults` as an alias for `count` here too. The
|
|
48
|
+
// search-mode handler already does this; list-mode used `args.count`
|
|
49
|
+
// only, so callers passing `maxResults=5` to a non-search list call
|
|
50
|
+
// saw their override silently ignored.
|
|
51
|
+
const requestedCount =
|
|
52
|
+
args.count ?? args.maxResults ?? DEFAULT_LIMITS.listEmails;
|
|
48
53
|
const verbosity = args.outputVerbosity || VERBOSITY.STANDARD;
|
|
49
54
|
|
|
50
55
|
try {
|
package/email/mail-tips.js
CHANGED
|
@@ -203,7 +203,7 @@ async function handleGetMailTips(args) {
|
|
|
203
203
|
content: [
|
|
204
204
|
{
|
|
205
205
|
type: 'text',
|
|
206
|
-
text: 'No mail tips returned
|
|
206
|
+
text: 'No mail tips returned. Mail Tips is M365-only — personal Outlook.com accounts return empty responses, so recipient validation is unavailable on this account.',
|
|
207
207
|
},
|
|
208
208
|
],
|
|
209
209
|
};
|
|
@@ -211,15 +211,39 @@ async function handleGetMailTips(args) {
|
|
|
211
211
|
|
|
212
212
|
const { formatted, warningCount } = formatMailTips(mailTips);
|
|
213
213
|
|
|
214
|
+
// F-23: Detect a "fully empty" tips response — every recipient
|
|
215
|
+
// returned with no actionable fields. Personal Outlook.com
|
|
216
|
+
// accounts surface this as a successful empty response rather
|
|
217
|
+
// than a feature-unsupported error, leading to false confidence
|
|
218
|
+
// when callers see "No issues detected".
|
|
219
|
+
const allEmpty = mailTips.every((tip) => {
|
|
220
|
+
const hasContent =
|
|
221
|
+
tip.recipientNotFound ||
|
|
222
|
+
tip.mailboxFull ||
|
|
223
|
+
tip.deliveryRestricted ||
|
|
224
|
+
tip.isModerated ||
|
|
225
|
+
tip.automaticReplies?.message ||
|
|
226
|
+
tip.maxMessageSize ||
|
|
227
|
+
tip.totalMemberCount ||
|
|
228
|
+
tip.customMailTip;
|
|
229
|
+
return !hasContent;
|
|
230
|
+
});
|
|
231
|
+
|
|
214
232
|
let header = `# Mail Tips\n\n`;
|
|
215
233
|
header += `**Recipients checked**: ${mailTips.length}\n`;
|
|
216
|
-
header += `**Warnings**: ${warningCount}\n
|
|
234
|
+
header += `**Warnings**: ${warningCount}\n`;
|
|
235
|
+
if (allEmpty && warningCount === 0) {
|
|
236
|
+
header +=
|
|
237
|
+
'\n**Note**: Graph returned no actionable mail tips for any recipient. This usually means Mail Tips is not supported on the connected account (M365-only feature) — "No issues detected" below means "no warnings flagged by Graph", NOT "validated as deliverable".\n';
|
|
238
|
+
}
|
|
239
|
+
header += '\n';
|
|
217
240
|
|
|
218
241
|
return {
|
|
219
242
|
content: [{ type: 'text', text: header + formatted }],
|
|
220
243
|
_meta: {
|
|
221
244
|
recipientCount: mailTips.length,
|
|
222
245
|
warningCount,
|
|
246
|
+
allEmpty,
|
|
223
247
|
},
|
|
224
248
|
};
|
|
225
249
|
} catch (error) {
|