@littlebearapps/outlook-assistant 3.3.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.
Files changed (52) hide show
  1. package/.env.example +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +422 -0
  4. package/advanced/index.js +652 -0
  5. package/auth/index.js +32 -0
  6. package/auth/oauth-server.js +233 -0
  7. package/auth/token-manager.js +105 -0
  8. package/auth/token-storage.js +359 -0
  9. package/auth/tools.js +159 -0
  10. package/calendar/accept.js +72 -0
  11. package/calendar/cancel.js +72 -0
  12. package/calendar/create.js +115 -0
  13. package/calendar/decline.js +72 -0
  14. package/calendar/delete.js +67 -0
  15. package/calendar/index.js +130 -0
  16. package/calendar/list.js +108 -0
  17. package/categories/index.js +955 -0
  18. package/config.js +95 -0
  19. package/contacts/index.js +754 -0
  20. package/email/attachments.js +365 -0
  21. package/email/conversations.js +666 -0
  22. package/email/delta.js +210 -0
  23. package/email/export.js +572 -0
  24. package/email/folder-utils.js +192 -0
  25. package/email/headers.js +344 -0
  26. package/email/index.js +537 -0
  27. package/email/list.js +136 -0
  28. package/email/mark-as-read.js +114 -0
  29. package/email/mime.js +286 -0
  30. package/email/read.js +161 -0
  31. package/email/search.js +628 -0
  32. package/email/send.js +169 -0
  33. package/folder/create.js +137 -0
  34. package/folder/delete.js +108 -0
  35. package/folder/index.js +112 -0
  36. package/folder/list.js +289 -0
  37. package/folder/move.js +186 -0
  38. package/folder/stats.js +322 -0
  39. package/index.js +162 -0
  40. package/llms.txt +76 -0
  41. package/outlook-auth-server.js +384 -0
  42. package/package.json +97 -0
  43. package/rules/create.js +273 -0
  44. package/rules/index.js +276 -0
  45. package/rules/list.js +216 -0
  46. package/settings/index.js +678 -0
  47. package/utils/field-presets.js +311 -0
  48. package/utils/graph-api.js +268 -0
  49. package/utils/mock-data.js +154 -0
  50. package/utils/odata-helpers.js +33 -0
  51. package/utils/response-formatter.js +457 -0
  52. package/utils/safety.js +123 -0
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Attachment handling for Outlook Assistant server
3
+ * Provides tools to list and download email attachments via Microsoft Graph API
4
+ */
5
+ const _https = require('https'); // Reserved for future use
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const _config = require('../config'); // Reserved for future use
9
+ const { callGraphAPI } = require('../utils/graph-api');
10
+ const { ensureAuthenticated } = require('../auth');
11
+
12
+ /**
13
+ * List attachments for a specific email
14
+ * @param {object} args - Tool arguments
15
+ * @param {string} args.messageId - The ID of the email message
16
+ * @returns {object} - MCP response with attachment list
17
+ */
18
+ async function handleListAttachments(args) {
19
+ const messageId = args.messageId;
20
+
21
+ if (!messageId) {
22
+ return {
23
+ content: [
24
+ {
25
+ type: 'text',
26
+ text: 'Error: messageId is required',
27
+ },
28
+ ],
29
+ };
30
+ }
31
+
32
+ try {
33
+ const accessToken = await ensureAuthenticated();
34
+
35
+ // Call Graph API to get attachments
36
+ const endpoint = `/me/messages/${messageId}/attachments`;
37
+ const params = {
38
+ $select: 'id,name,contentType,size,isInline',
39
+ };
40
+
41
+ console.error(`Fetching attachments for message: ${messageId}`);
42
+ const response = await callGraphAPI(
43
+ accessToken,
44
+ 'GET',
45
+ endpoint,
46
+ null,
47
+ params
48
+ );
49
+
50
+ if (!response.value || response.value.length === 0) {
51
+ return {
52
+ content: [
53
+ {
54
+ type: 'text',
55
+ text: 'No attachments found for this email.',
56
+ },
57
+ ],
58
+ };
59
+ }
60
+
61
+ // Format attachment list
62
+ const attachmentList = response.value
63
+ .map((att, index) => {
64
+ const sizeKB = (att.size / 1024).toFixed(1);
65
+ const inline = att.isInline ? ' [inline]' : '';
66
+ return `${index + 1}. ${att.name}${inline}\n Type: ${att.contentType}\n Size: ${sizeKB} KB\n ID: ${att.id}`;
67
+ })
68
+ .join('\n\n');
69
+
70
+ return {
71
+ content: [
72
+ {
73
+ type: 'text',
74
+ text: `Found ${response.value.length} attachment(s):\n\n${attachmentList}`,
75
+ },
76
+ ],
77
+ };
78
+ } catch (error) {
79
+ if (
80
+ error.message === 'Authentication required' ||
81
+ error.message === 'UNAUTHORIZED'
82
+ ) {
83
+ return {
84
+ content: [
85
+ {
86
+ type: 'text',
87
+ text: "Authentication required. Please use the 'authenticate' tool first.",
88
+ },
89
+ ],
90
+ };
91
+ }
92
+
93
+ return {
94
+ content: [
95
+ {
96
+ type: 'text',
97
+ text: `Error listing attachments: ${error.message}`,
98
+ },
99
+ ],
100
+ };
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Download a specific attachment
106
+ * @param {object} args - Tool arguments
107
+ * @param {string} args.messageId - The ID of the email message
108
+ * @param {string} args.attachmentId - The ID of the attachment
109
+ * @param {string} args.savePath - Optional path to save the file (defaults to current directory)
110
+ * @returns {object} - MCP response with download result
111
+ */
112
+ async function handleDownloadAttachment(args) {
113
+ const { messageId, attachmentId, savePath } = args;
114
+
115
+ if (!messageId || !attachmentId) {
116
+ return {
117
+ content: [
118
+ {
119
+ type: 'text',
120
+ text: 'Error: Both messageId and attachmentId are required',
121
+ },
122
+ ],
123
+ };
124
+ }
125
+
126
+ try {
127
+ const accessToken = await ensureAuthenticated();
128
+
129
+ // First, get attachment metadata to get the filename and content
130
+ const metadataEndpoint = `/me/messages/${messageId}/attachments/${attachmentId}`;
131
+ console.error(`Fetching attachment metadata: ${attachmentId}`);
132
+
133
+ const metadata = await callGraphAPI(
134
+ accessToken,
135
+ 'GET',
136
+ metadataEndpoint,
137
+ null,
138
+ {}
139
+ );
140
+
141
+ if (!metadata) {
142
+ return {
143
+ content: [
144
+ {
145
+ type: 'text',
146
+ text: 'Error: Attachment not found',
147
+ },
148
+ ],
149
+ };
150
+ }
151
+
152
+ const filename = metadata.name || 'attachment';
153
+ const contentType = metadata.contentType || 'application/octet-stream';
154
+
155
+ // For file attachments, the content is base64 encoded in contentBytes
156
+ if (metadata['@odata.type'] === '#microsoft.graph.fileAttachment') {
157
+ const contentBytes = metadata.contentBytes;
158
+
159
+ if (!contentBytes) {
160
+ return {
161
+ content: [
162
+ {
163
+ type: 'text',
164
+ text: 'Error: No content found in attachment',
165
+ },
166
+ ],
167
+ };
168
+ }
169
+
170
+ // Determine save location
171
+ const outputDir = savePath || process.cwd();
172
+ const outputPath = path.join(outputDir, filename);
173
+
174
+ // Decode base64 and save to file
175
+ const buffer = Buffer.from(contentBytes, 'base64');
176
+ fs.writeFileSync(outputPath, buffer);
177
+
178
+ const sizeKB = (buffer.length / 1024).toFixed(1);
179
+
180
+ return {
181
+ content: [
182
+ {
183
+ type: 'text',
184
+ text: `Successfully downloaded attachment:\n\nFilename: ${filename}\nType: ${contentType}\nSize: ${sizeKB} KB\nSaved to: ${outputPath}`,
185
+ },
186
+ ],
187
+ };
188
+ } else if (metadata['@odata.type'] === '#microsoft.graph.itemAttachment') {
189
+ // Item attachments (embedded emails, calendar items) need different handling
190
+ return {
191
+ content: [
192
+ {
193
+ type: 'text',
194
+ text: `This is an embedded item attachment (${metadata.name}). Item attachments cannot be downloaded as files directly. They contain embedded Outlook items like emails or calendar events.`,
195
+ },
196
+ ],
197
+ };
198
+ } else if (
199
+ metadata['@odata.type'] === '#microsoft.graph.referenceAttachment'
200
+ ) {
201
+ // Reference attachments are links to cloud files
202
+ return {
203
+ content: [
204
+ {
205
+ type: 'text',
206
+ text: `This is a reference attachment (cloud link):\n\nName: ${metadata.name}\nThis attachment is a link to a file stored in the cloud and cannot be downloaded directly.`,
207
+ },
208
+ ],
209
+ };
210
+ } else {
211
+ return {
212
+ content: [
213
+ {
214
+ type: 'text',
215
+ text: `Unknown attachment type: ${metadata['@odata.type']}`,
216
+ },
217
+ ],
218
+ };
219
+ }
220
+ } catch (error) {
221
+ if (
222
+ error.message === 'Authentication required' ||
223
+ error.message === 'UNAUTHORIZED'
224
+ ) {
225
+ return {
226
+ content: [
227
+ {
228
+ type: 'text',
229
+ text: "Authentication required. Please use the 'authenticate' tool first.",
230
+ },
231
+ ],
232
+ };
233
+ }
234
+
235
+ return {
236
+ content: [
237
+ {
238
+ type: 'text',
239
+ text: `Error downloading attachment: ${error.message}`,
240
+ },
241
+ ],
242
+ };
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Get attachment content as base64 (useful for viewing in Claude)
248
+ * @param {object} args - Tool arguments
249
+ * @param {string} args.messageId - The ID of the email message
250
+ * @param {string} args.attachmentId - The ID of the attachment
251
+ * @returns {object} - MCP response with attachment content
252
+ */
253
+ async function handleGetAttachmentContent(args) {
254
+ const { messageId, attachmentId } = args;
255
+
256
+ if (!messageId || !attachmentId) {
257
+ return {
258
+ content: [
259
+ {
260
+ type: 'text',
261
+ text: 'Error: Both messageId and attachmentId are required',
262
+ },
263
+ ],
264
+ };
265
+ }
266
+
267
+ try {
268
+ const accessToken = await ensureAuthenticated();
269
+
270
+ const endpoint = `/me/messages/${messageId}/attachments/${attachmentId}`;
271
+ console.error(`Fetching attachment content: ${attachmentId}`);
272
+
273
+ const response = await callGraphAPI(accessToken, 'GET', endpoint, null, {});
274
+
275
+ if (!response) {
276
+ return {
277
+ content: [
278
+ {
279
+ type: 'text',
280
+ text: 'Error: Attachment not found',
281
+ },
282
+ ],
283
+ };
284
+ }
285
+
286
+ const filename = response.name || 'attachment';
287
+ const contentType = response.contentType || 'application/octet-stream';
288
+ const sizeBytes = response.size || 0;
289
+ const sizeKB = (sizeBytes / 1024).toFixed(1);
290
+
291
+ // For file attachments, return metadata and indicate content is available
292
+ if (response['@odata.type'] === '#microsoft.graph.fileAttachment') {
293
+ // Check if it's a text-based file we can display
294
+ const textTypes = [
295
+ 'text/',
296
+ 'application/json',
297
+ 'application/xml',
298
+ 'application/javascript',
299
+ ];
300
+ const isText = textTypes.some((t) => contentType.startsWith(t));
301
+
302
+ if (isText && response.contentBytes) {
303
+ const content = Buffer.from(response.contentBytes, 'base64').toString(
304
+ 'utf-8'
305
+ );
306
+ return {
307
+ content: [
308
+ {
309
+ type: 'text',
310
+ text: `Attachment: ${filename}\nType: ${contentType}\nSize: ${sizeKB} KB\n\n--- Content ---\n${content}`,
311
+ },
312
+ ],
313
+ };
314
+ }
315
+
316
+ // For binary files, just return metadata
317
+ return {
318
+ content: [
319
+ {
320
+ type: 'text',
321
+ text: `Attachment: ${filename}\nType: ${contentType}\nSize: ${sizeKB} KB\n\nThis is a binary file. Use 'download-attachment' to save it to disk.`,
322
+ },
323
+ ],
324
+ };
325
+ }
326
+
327
+ return {
328
+ content: [
329
+ {
330
+ type: 'text',
331
+ text: `Attachment: ${filename}\nType: ${response['@odata.type']}\nSize: ${sizeKB} KB`,
332
+ },
333
+ ],
334
+ };
335
+ } catch (error) {
336
+ if (
337
+ error.message === 'Authentication required' ||
338
+ error.message === 'UNAUTHORIZED'
339
+ ) {
340
+ return {
341
+ content: [
342
+ {
343
+ type: 'text',
344
+ text: "Authentication required. Please use the 'authenticate' tool first.",
345
+ },
346
+ ],
347
+ };
348
+ }
349
+
350
+ return {
351
+ content: [
352
+ {
353
+ type: 'text',
354
+ text: `Error getting attachment content: ${error.message}`,
355
+ },
356
+ ],
357
+ };
358
+ }
359
+ }
360
+
361
+ module.exports = {
362
+ handleListAttachments,
363
+ handleDownloadAttachment,
364
+ handleGetAttachmentContent,
365
+ };