outlook-cli 1.2.1 → 1.2.3
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/CLI.md +16 -0
- package/README.md +110 -5
- package/cli.js +483 -70
- package/docs/REFERENCE.md +40 -2
- package/email/attachments.js +359 -0
- package/email/index.js +58 -1
- package/email/search.js +18 -12
- package/package.json +1 -1
- package/utils/graph-api.js +132 -44
package/docs/REFERENCE.md
CHANGED
|
@@ -11,7 +11,8 @@ This document is the complete, publish-ready reference for:
|
|
|
11
11
|
- CLI binary: `outlook-cli`
|
|
12
12
|
- Output mode:
|
|
13
13
|
- `text` (default)
|
|
14
|
-
- `json` (with `--json
|
|
14
|
+
- `json` (with `--json`, `--ai`, or `--output json`)
|
|
15
|
+
- JSON responses include `structured` for normalized, machine-friendly fields while preserving raw `result`.
|
|
15
16
|
- Option key normalization:
|
|
16
17
|
- `--event-id` and `--eventId` map to the same internal key.
|
|
17
18
|
- `--start-server` and `--startServer` are equivalent.
|
|
@@ -24,7 +25,9 @@ This document is the complete, publish-ready reference for:
|
|
|
24
25
|
| Option | Type | Required | Default | Description |
|
|
25
26
|
|---|---|---:|---|---|
|
|
26
27
|
| `--json` | boolean | No | `false` | Forces JSON output for automation and agents. |
|
|
28
|
+
| `--ai` | boolean | No | `false` | Alias for JSON-first agent mode (no rich UI noise). |
|
|
27
29
|
| `--output` | `text` \| `json` | No | `text` | Explicit output mode override. |
|
|
30
|
+
| `--theme` | `k9s` \| `ocean` \| `mono` | No | `k9s` | Text-mode color theme preset. |
|
|
28
31
|
| `--plain` | boolean | No | `false` | Disables rich color and animation UI output. |
|
|
29
32
|
| `--no-color` | boolean | No | `false` | Disables terminal colors. |
|
|
30
33
|
| `--no-animate` | boolean | No | `false` | Disables loading/waiting spinner animation. |
|
|
@@ -40,11 +43,13 @@ This document is the complete, publish-ready reference for:
|
|
|
40
43
|
| `tools list` | Lists all tools with descriptions and schemas. |
|
|
41
44
|
| `tools schema <tool-name>` | Prints schema for one tool. |
|
|
42
45
|
| `call <tool-name>` | Calls any registered MCP tool directly. |
|
|
46
|
+
| `agents guide` | Prints AI-agent workflow guidance and best practices. |
|
|
43
47
|
| `auth ...` | Authentication command group. |
|
|
44
48
|
| `email ...` | Email command group. |
|
|
45
49
|
| `calendar ...` | Calendar command group. |
|
|
46
50
|
| `folder ...` | Folder command group. |
|
|
47
51
|
| `rule ...` | Rules command group. |
|
|
52
|
+
| `mcp-server` | Starts the stdio MCP server (same behavior as `node index.js`). |
|
|
48
53
|
| `doctor` | Environment and auth diagnostics. |
|
|
49
54
|
| `update` | Prints or runs global npm update command. |
|
|
50
55
|
| `help` | Same as `--help`. |
|
|
@@ -118,6 +123,18 @@ Examples:
|
|
|
118
123
|
```bash
|
|
119
124
|
outlook-cli call list-emails --args-json '{"count":5}'
|
|
120
125
|
outlook-cli call search-emails --arg query=invoice --arg unreadOnly=true --json
|
|
126
|
+
outlook-cli call send-email --args-json '{"to":"a@example.com","subject":"Hi","body":"Hello"}' --ai
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### agents guide
|
|
130
|
+
|
|
131
|
+
Prints AI-agent command patterns and orchestration best practices.
|
|
132
|
+
|
|
133
|
+
Usage:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
outlook-cli agents guide
|
|
137
|
+
outlook-cli agents guide --json
|
|
121
138
|
```
|
|
122
139
|
|
|
123
140
|
### auth status
|
|
@@ -137,7 +154,7 @@ Prints the Microsoft OAuth URL generated from current config.
|
|
|
137
154
|
Usage:
|
|
138
155
|
|
|
139
156
|
```bash
|
|
140
|
-
outlook-cli auth url
|
|
157
|
+
outlook-cli auth url [--client-id <id>]
|
|
141
158
|
```
|
|
142
159
|
|
|
143
160
|
### auth login
|
|
@@ -159,6 +176,27 @@ Options:
|
|
|
159
176
|
| `--start-server` | boolean | No | `true` | Starts local auth server if not already running. |
|
|
160
177
|
| `--wait` | boolean | No | `true` | Waits for token completion before returning. |
|
|
161
178
|
| `--timeout` | number (seconds) | No | `180` | Max wait time when `--wait` is true. Min effective timeout is 5s. |
|
|
179
|
+
| `--client-id` | string | No | env/config | Runtime override for client ID. |
|
|
180
|
+
| `--client-secret` | string | No | env/config | Runtime override for client secret value. |
|
|
181
|
+
| `--prompt-credentials` | boolean | No | `true` | Prompt for missing credentials in interactive terminals. |
|
|
182
|
+
|
|
183
|
+
### auth server
|
|
184
|
+
|
|
185
|
+
Checks or starts the local OAuth callback server.
|
|
186
|
+
|
|
187
|
+
Usage:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
outlook-cli auth server --status
|
|
191
|
+
outlook-cli auth server --start
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Options:
|
|
195
|
+
|
|
196
|
+
| Option | Type | Required | Default | Description |
|
|
197
|
+
|---|---|---:|---|---|
|
|
198
|
+
| `--status` | boolean | No | `true` when `--start` is not passed | Prints reachability status only. |
|
|
199
|
+
| `--start` | boolean | No | `false` | Starts callback server in background if not running. |
|
|
162
200
|
|
|
163
201
|
### auth logout
|
|
164
202
|
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email attachment listing and retrieval functionality
|
|
3
|
+
*/
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { callGraphAPI, callGraphAPIRaw } = require('../utils/graph-api');
|
|
7
|
+
const { ensureAuthenticated } = require('../auth');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_ATTACHMENT_COUNT = 25;
|
|
10
|
+
const MAX_ATTACHMENT_COUNT = 50;
|
|
11
|
+
|
|
12
|
+
function textResponse(text) {
|
|
13
|
+
return {
|
|
14
|
+
content: [{
|
|
15
|
+
type: 'text',
|
|
16
|
+
text
|
|
17
|
+
}]
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function asBoolean(value, defaultValue = false) {
|
|
22
|
+
if (value === undefined || value === null || value === '') {
|
|
23
|
+
return defaultValue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (typeof value === 'boolean') {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (typeof value === 'number') {
|
|
31
|
+
return value !== 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const normalized = String(value).trim().toLowerCase();
|
|
35
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return defaultValue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function asCount(value, defaultValue = DEFAULT_ATTACHMENT_COUNT) {
|
|
47
|
+
const parsed = Number(value);
|
|
48
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
49
|
+
return defaultValue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return Math.min(parsed, MAX_ATTACHMENT_COUNT);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getAttachmentKind(rawType) {
|
|
56
|
+
const type = String(rawType || '').toLowerCase();
|
|
57
|
+
|
|
58
|
+
if (type.includes('fileattachment')) {
|
|
59
|
+
return 'fileAttachment';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (type.includes('itemattachment')) {
|
|
63
|
+
return 'itemAttachment';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (type.includes('referenceattachment')) {
|
|
67
|
+
return 'referenceAttachment';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return 'attachment';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function sanitizeFileName(value) {
|
|
74
|
+
const raw = String(value || '').trim();
|
|
75
|
+
if (!raw) {
|
|
76
|
+
return 'attachment.bin';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const sanitized = raw
|
|
80
|
+
.replace(/[<>:"/\\|?*\u0000-\u001F]/g, '_')
|
|
81
|
+
.replace(/\s+/g, ' ')
|
|
82
|
+
.trim();
|
|
83
|
+
|
|
84
|
+
return sanitized || 'attachment.bin';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function shouldTreatAsDirectory(inputPath) {
|
|
88
|
+
const value = String(inputPath || '');
|
|
89
|
+
return value.endsWith(path.sep) || value.endsWith('/') || value.endsWith('\\');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resolveOutputFilePath(savePath, fallbackFileName) {
|
|
93
|
+
const requested = String(savePath || '').trim();
|
|
94
|
+
if (!requested) {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const resolved = path.resolve(requested);
|
|
99
|
+
if (fs.existsSync(resolved)) {
|
|
100
|
+
const stats = fs.statSync(resolved);
|
|
101
|
+
if (stats.isDirectory()) {
|
|
102
|
+
return path.join(resolved, fallbackFileName);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return resolved;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (shouldTreatAsDirectory(requested)) {
|
|
109
|
+
return path.join(resolved, fallbackFileName);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return resolved;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function ensureOutputWritable(filePath, overwrite) {
|
|
116
|
+
if (fs.existsSync(filePath) && !overwrite) {
|
|
117
|
+
throw new Error(`File already exists at ${filePath}. Re-run with overwrite=true to replace it.`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function looksLikeText(buffer) {
|
|
124
|
+
if (!buffer || buffer.length === 0) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const sampleSize = Math.min(buffer.length, 2048);
|
|
129
|
+
let printableCount = 0;
|
|
130
|
+
|
|
131
|
+
for (let index = 0; index < sampleSize; index += 1) {
|
|
132
|
+
const byte = buffer[index];
|
|
133
|
+
const isPrintable = byte === 9 || byte === 10 || byte === 13 || (byte >= 32 && byte <= 126);
|
|
134
|
+
if (isPrintable) {
|
|
135
|
+
printableCount += 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return (printableCount / sampleSize) >= 0.8;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildContentPreview(buffer) {
|
|
143
|
+
if (!looksLikeText(buffer)) {
|
|
144
|
+
return '';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return buffer
|
|
148
|
+
.toString('utf8', 0, Math.min(buffer.length, 4096))
|
|
149
|
+
.slice(0, 600)
|
|
150
|
+
.trim();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function defaultFileName(attachment, attachmentId) {
|
|
154
|
+
const kind = getAttachmentKind(attachment['@odata.type']);
|
|
155
|
+
const fromName = sanitizeFileName(attachment.name || '');
|
|
156
|
+
|
|
157
|
+
if (fromName !== 'attachment.bin') {
|
|
158
|
+
if (kind === 'itemAttachment' && !path.extname(fromName)) {
|
|
159
|
+
return `${fromName}.eml`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return fromName;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (kind === 'itemAttachment') {
|
|
166
|
+
return `attachment-${attachmentId}.eml`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return `attachment-${attachmentId}.bin`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function getAttachmentBytes(accessToken, messageId, attachment, attachmentId) {
|
|
173
|
+
const kind = getAttachmentKind(attachment['@odata.type']);
|
|
174
|
+
|
|
175
|
+
if (kind === 'referenceAttachment') {
|
|
176
|
+
throw new Error('Reference attachments are cloud links and cannot be downloaded as raw bytes via this endpoint.');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (kind === 'fileAttachment' && attachment.contentBytes) {
|
|
180
|
+
return Buffer.from(attachment.contentBytes, 'base64');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const rawResponse = await callGraphAPIRaw(
|
|
184
|
+
accessToken,
|
|
185
|
+
'GET',
|
|
186
|
+
`me/messages/${messageId}/attachments/${attachmentId}/$value`
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return rawResponse.body;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function formatAttachmentLine(attachment, index) {
|
|
193
|
+
const type = getAttachmentKind(attachment['@odata.type']);
|
|
194
|
+
const modified = attachment.lastModifiedDateTime
|
|
195
|
+
? new Date(attachment.lastModifiedDateTime).toLocaleString()
|
|
196
|
+
: 'Unknown';
|
|
197
|
+
|
|
198
|
+
return `${index + 1}. ${attachment.name || '(unnamed attachment)'}
|
|
199
|
+
ID: ${attachment.id}
|
|
200
|
+
Type: ${type}
|
|
201
|
+
Content Type: ${attachment.contentType || 'unknown'}
|
|
202
|
+
Size: ${attachment.size || 0} bytes
|
|
203
|
+
Inline: ${attachment.isInline ? 'Yes' : 'No'}
|
|
204
|
+
Last Modified: ${modified}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* List attachments for an email message
|
|
209
|
+
* @param {object} args - Tool arguments
|
|
210
|
+
* @returns {object} - MCP response
|
|
211
|
+
*/
|
|
212
|
+
async function handleListAttachments(args = {}) {
|
|
213
|
+
const messageId = args.messageId || args.id;
|
|
214
|
+
const count = asCount(args.count, DEFAULT_ATTACHMENT_COUNT);
|
|
215
|
+
|
|
216
|
+
if (!messageId) {
|
|
217
|
+
return textResponse('Message ID is required.');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const accessToken = await ensureAuthenticated();
|
|
222
|
+
|
|
223
|
+
const response = await callGraphAPI(
|
|
224
|
+
accessToken,
|
|
225
|
+
'GET',
|
|
226
|
+
`me/messages/${messageId}/attachments`,
|
|
227
|
+
null,
|
|
228
|
+
{
|
|
229
|
+
$top: count,
|
|
230
|
+
$select: 'id,name,contentType,size,isInline,lastModifiedDateTime'
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const attachments = Array.isArray(response.value) ? response.value : [];
|
|
235
|
+
|
|
236
|
+
if (attachments.length === 0) {
|
|
237
|
+
return textResponse(`No attachments found for message ${messageId}.`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const formatted = attachments
|
|
241
|
+
.map((attachment, index) => formatAttachmentLine(attachment, index))
|
|
242
|
+
.join('\n\n');
|
|
243
|
+
|
|
244
|
+
return textResponse(`Found ${attachments.length} attachment(s) for message ${messageId}:\n\n${formatted}`);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (error.message === 'Authentication required') {
|
|
247
|
+
return textResponse("Authentication required. Please use the 'authenticate' tool first.");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return textResponse(`Error listing attachments: ${error.message}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get one attachment's details and optionally save it to disk
|
|
256
|
+
* @param {object} args - Tool arguments
|
|
257
|
+
* @returns {object} - MCP response
|
|
258
|
+
*/
|
|
259
|
+
async function handleGetAttachment(args = {}) {
|
|
260
|
+
const messageId = args.messageId || args.id;
|
|
261
|
+
const attachmentId = args.attachmentId;
|
|
262
|
+
const savePath = args.savePath || args.outputPath || args.out;
|
|
263
|
+
const includeContent = asBoolean(args.includeContent, false);
|
|
264
|
+
const expandItem = asBoolean(args.expandItem, false);
|
|
265
|
+
const overwrite = asBoolean(args.overwrite, false);
|
|
266
|
+
|
|
267
|
+
if (!messageId) {
|
|
268
|
+
return textResponse('Message ID is required.');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!attachmentId) {
|
|
272
|
+
return textResponse('Attachment ID is required.');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const accessToken = await ensureAuthenticated();
|
|
277
|
+
const queryParams = {};
|
|
278
|
+
|
|
279
|
+
if (expandItem) {
|
|
280
|
+
queryParams.$expand = 'microsoft.graph.itemattachment/item';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const attachment = await callGraphAPI(
|
|
284
|
+
accessToken,
|
|
285
|
+
'GET',
|
|
286
|
+
`me/messages/${messageId}/attachments/${attachmentId}`,
|
|
287
|
+
null,
|
|
288
|
+
queryParams
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
if (!attachment || !attachment.id) {
|
|
292
|
+
return textResponse(`Attachment ${attachmentId} not found in message ${messageId}.`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const kind = getAttachmentKind(attachment['@odata.type']);
|
|
296
|
+
const summary = [
|
|
297
|
+
`Attachment: ${attachment.name || '(unnamed attachment)'}`,
|
|
298
|
+
`ID: ${attachment.id}`,
|
|
299
|
+
`Type: ${kind}`,
|
|
300
|
+
`Content Type: ${attachment.contentType || 'unknown'}`,
|
|
301
|
+
`Size: ${attachment.size || 0} bytes`,
|
|
302
|
+
`Inline: ${attachment.isInline ? 'Yes' : 'No'}`
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
if (kind === 'referenceAttachment') {
|
|
306
|
+
const sourceUrl = attachment.sourceUrl || attachment.providerType || 'Unknown';
|
|
307
|
+
summary.push(`Reference Target: ${sourceUrl}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (attachment.item && typeof attachment.item === 'object') {
|
|
311
|
+
summary.push(`Attached Item Type: ${attachment.item['@odata.type'] || 'unknown'}`);
|
|
312
|
+
if (attachment.item.subject) {
|
|
313
|
+
summary.push(`Attached Item Subject: ${attachment.item.subject}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let downloadedBuffer = null;
|
|
318
|
+
if (savePath || includeContent) {
|
|
319
|
+
downloadedBuffer = await getAttachmentBytes(accessToken, messageId, attachment, attachmentId);
|
|
320
|
+
summary.push(`Downloaded Bytes: ${downloadedBuffer.length}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (savePath) {
|
|
324
|
+
const destination = resolveOutputFilePath(
|
|
325
|
+
savePath,
|
|
326
|
+
defaultFileName(attachment, attachmentId)
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
ensureOutputWritable(destination, overwrite);
|
|
330
|
+
fs.writeFileSync(destination, downloadedBuffer);
|
|
331
|
+
summary.push(`Saved To: ${destination}`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (includeContent && downloadedBuffer) {
|
|
335
|
+
const preview = buildContentPreview(downloadedBuffer);
|
|
336
|
+
if (preview) {
|
|
337
|
+
summary.push('');
|
|
338
|
+
summary.push('Content Preview:');
|
|
339
|
+
summary.push(preview);
|
|
340
|
+
} else {
|
|
341
|
+
summary.push('');
|
|
342
|
+
summary.push('Content Preview: Binary content detected (no text preview).');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return textResponse(summary.join('\n'));
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (error.message === 'Authentication required') {
|
|
349
|
+
return textResponse("Authentication required. Please use the 'authenticate' tool first.");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return textResponse(`Error getting attachment: ${error.message}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
module.exports = {
|
|
357
|
+
handleListAttachments,
|
|
358
|
+
handleGetAttachment
|
|
359
|
+
};
|
package/email/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const handleSearchEmails = require('./search');
|
|
|
6
6
|
const handleReadEmail = require('./read');
|
|
7
7
|
const handleSendEmail = require('./send');
|
|
8
8
|
const handleMarkAsRead = require('./mark-as-read');
|
|
9
|
+
const { handleListAttachments, handleGetAttachment } = require('./attachments');
|
|
9
10
|
|
|
10
11
|
// Email tool definitions
|
|
11
12
|
const emailTools = [
|
|
@@ -144,6 +145,60 @@ const emailTools = [
|
|
|
144
145
|
required: ["id"]
|
|
145
146
|
},
|
|
146
147
|
handler: handleMarkAsRead
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "list-attachments",
|
|
151
|
+
description: "Lists attachments for a specific email",
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: "object",
|
|
154
|
+
properties: {
|
|
155
|
+
messageId: {
|
|
156
|
+
type: "string",
|
|
157
|
+
description: "ID of the message that contains attachments"
|
|
158
|
+
},
|
|
159
|
+
count: {
|
|
160
|
+
type: "number",
|
|
161
|
+
description: "Maximum number of attachments to list (default: 25, max: 50)"
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
required: ["messageId"]
|
|
165
|
+
},
|
|
166
|
+
handler: handleListAttachments
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "get-attachment",
|
|
170
|
+
description: "Gets one attachment's metadata and optionally downloads it",
|
|
171
|
+
inputSchema: {
|
|
172
|
+
type: "object",
|
|
173
|
+
properties: {
|
|
174
|
+
messageId: {
|
|
175
|
+
type: "string",
|
|
176
|
+
description: "ID of the message that contains the attachment"
|
|
177
|
+
},
|
|
178
|
+
attachmentId: {
|
|
179
|
+
type: "string",
|
|
180
|
+
description: "ID of the attachment to read or download"
|
|
181
|
+
},
|
|
182
|
+
savePath: {
|
|
183
|
+
type: "string",
|
|
184
|
+
description: "Optional output file path or output directory path"
|
|
185
|
+
},
|
|
186
|
+
includeContent: {
|
|
187
|
+
type: "boolean",
|
|
188
|
+
description: "When true, include a text preview for text-like attachments"
|
|
189
|
+
},
|
|
190
|
+
expandItem: {
|
|
191
|
+
type: "boolean",
|
|
192
|
+
description: "When true, expand item attachment metadata"
|
|
193
|
+
},
|
|
194
|
+
overwrite: {
|
|
195
|
+
type: "boolean",
|
|
196
|
+
description: "When true, overwrite an existing file at savePath"
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
required: ["messageId", "attachmentId"]
|
|
200
|
+
},
|
|
201
|
+
handler: handleGetAttachment
|
|
147
202
|
}
|
|
148
203
|
];
|
|
149
204
|
|
|
@@ -153,5 +208,7 @@ module.exports = {
|
|
|
153
208
|
handleSearchEmails,
|
|
154
209
|
handleReadEmail,
|
|
155
210
|
handleSendEmail,
|
|
156
|
-
handleMarkAsRead
|
|
211
|
+
handleMarkAsRead,
|
|
212
|
+
handleListAttachments,
|
|
213
|
+
handleGetAttachment
|
|
157
214
|
};
|
package/email/search.js
CHANGED
|
@@ -6,6 +6,12 @@ const { callGraphAPI } = require('../utils/graph-api');
|
|
|
6
6
|
const { ensureAuthenticated } = require('../auth');
|
|
7
7
|
const { resolveFolderPath } = require('./folder-utils');
|
|
8
8
|
|
|
9
|
+
function debugLog(...args) {
|
|
10
|
+
if (config.DEBUG_LOGS) {
|
|
11
|
+
console.error(...args);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
/**
|
|
10
16
|
* Search emails handler
|
|
11
17
|
* @param {object} args - Tool arguments
|
|
@@ -27,7 +33,7 @@ async function handleSearchEmails(args) {
|
|
|
27
33
|
|
|
28
34
|
// Resolve the folder path
|
|
29
35
|
const endpoint = await resolveFolderPath(accessToken, folder);
|
|
30
|
-
|
|
36
|
+
debugLog(`Using endpoint: ${endpoint} for folder: ${folder}`);
|
|
31
37
|
|
|
32
38
|
// Execute progressive search
|
|
33
39
|
const response = await progressiveSearch(
|
|
@@ -76,16 +82,16 @@ async function progressiveSearch(endpoint, accessToken, searchTerms, filterTerms
|
|
|
76
82
|
// 1. Try combined search (most specific)
|
|
77
83
|
try {
|
|
78
84
|
const params = buildSearchParams(searchTerms, filterTerms, count);
|
|
79
|
-
|
|
85
|
+
debugLog('Attempting combined search with params:', params);
|
|
80
86
|
searchAttempts.push("combined-search");
|
|
81
87
|
|
|
82
88
|
const response = await callGraphAPI(accessToken, 'GET', endpoint, null, params);
|
|
83
89
|
if (response.value && response.value.length > 0) {
|
|
84
|
-
|
|
90
|
+
debugLog(`Combined search successful: found ${response.value.length} results`);
|
|
85
91
|
return response;
|
|
86
92
|
}
|
|
87
93
|
} catch (error) {
|
|
88
|
-
|
|
94
|
+
debugLog(`Combined search failed: ${error.message}`);
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
// 2. Try each search term individually, starting with most specific
|
|
@@ -94,7 +100,7 @@ async function progressiveSearch(endpoint, accessToken, searchTerms, filterTerms
|
|
|
94
100
|
for (const term of searchPriority) {
|
|
95
101
|
if (searchTerms[term]) {
|
|
96
102
|
try {
|
|
97
|
-
|
|
103
|
+
debugLog(`Attempting search with only ${term}: "${searchTerms[term]}"`);
|
|
98
104
|
searchAttempts.push(`single-term-${term}`);
|
|
99
105
|
|
|
100
106
|
// For single term search, only use $search with that term
|
|
@@ -118,11 +124,11 @@ async function progressiveSearch(endpoint, accessToken, searchTerms, filterTerms
|
|
|
118
124
|
|
|
119
125
|
const response = await callGraphAPI(accessToken, 'GET', endpoint, null, simplifiedParams);
|
|
120
126
|
if (response.value && response.value.length > 0) {
|
|
121
|
-
|
|
127
|
+
debugLog(`Search with ${term} successful: found ${response.value.length} results`);
|
|
122
128
|
return response;
|
|
123
129
|
}
|
|
124
130
|
} catch (error) {
|
|
125
|
-
|
|
131
|
+
debugLog(`Search with ${term} failed: ${error.message}`);
|
|
126
132
|
}
|
|
127
133
|
}
|
|
128
134
|
}
|
|
@@ -130,7 +136,7 @@ async function progressiveSearch(endpoint, accessToken, searchTerms, filterTerms
|
|
|
130
136
|
// 3. Try with only boolean filters
|
|
131
137
|
if (filterTerms.hasAttachments === true || filterTerms.unreadOnly === true) {
|
|
132
138
|
try {
|
|
133
|
-
|
|
139
|
+
debugLog('Attempting search with only boolean filters');
|
|
134
140
|
searchAttempts.push("boolean-filters-only");
|
|
135
141
|
|
|
136
142
|
const filterOnlyParams = {
|
|
@@ -143,15 +149,15 @@ async function progressiveSearch(endpoint, accessToken, searchTerms, filterTerms
|
|
|
143
149
|
addBooleanFilters(filterOnlyParams, filterTerms);
|
|
144
150
|
|
|
145
151
|
const response = await callGraphAPI(accessToken, 'GET', endpoint, null, filterOnlyParams);
|
|
146
|
-
|
|
152
|
+
debugLog(`Boolean filter search found ${response.value?.length || 0} results`);
|
|
147
153
|
return response;
|
|
148
154
|
} catch (error) {
|
|
149
|
-
|
|
155
|
+
debugLog(`Boolean filter search failed: ${error.message}`);
|
|
150
156
|
}
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
// 4. Final fallback: just get recent emails
|
|
154
|
-
|
|
160
|
+
debugLog('All search strategies failed, falling back to recent emails');
|
|
155
161
|
searchAttempts.push("recent-emails");
|
|
156
162
|
|
|
157
163
|
const basicParams = {
|
|
@@ -161,7 +167,7 @@ async function progressiveSearch(endpoint, accessToken, searchTerms, filterTerms
|
|
|
161
167
|
};
|
|
162
168
|
|
|
163
169
|
const response = await callGraphAPI(accessToken, 'GET', endpoint, null, basicParams);
|
|
164
|
-
|
|
170
|
+
debugLog(`Fallback to recent emails found ${response.value?.length || 0} results`);
|
|
165
171
|
|
|
166
172
|
// Add a note to the response about the search attempts
|
|
167
173
|
response._searchInfo = {
|