@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
package/email/index.js ADDED
@@ -0,0 +1,537 @@
1
+ /**
2
+ * Email module for Outlook Assistant server
3
+ *
4
+ * Consolidated from 17 tools to 6 for token efficiency.
5
+ */
6
+ const handleListEmails = require('./list');
7
+ const { handleSearchEmails, handleSearchByMessageId } = require('./search');
8
+ const handleReadEmail = require('./read');
9
+ const handleSendEmail = require('./send');
10
+ const handleMarkAsRead = require('./mark-as-read');
11
+ const {
12
+ handleListAttachments,
13
+ handleDownloadAttachment,
14
+ handleGetAttachmentContent,
15
+ } = require('./attachments');
16
+ const { handleExportEmail, handleBatchExportEmails } = require('./export');
17
+ const handleListEmailsDelta = require('./delta');
18
+ const { handleGetEmailHeaders } = require('./headers');
19
+ const { handleGetMimeContent } = require('./mime');
20
+ const {
21
+ handleListConversations,
22
+ handleGetConversation,
23
+ handleExportConversation,
24
+ } = require('./conversations');
25
+
26
+ // Import flag handlers from advanced module
27
+ const { handleSetMessageFlag, handleClearMessageFlag } = require('../advanced');
28
+
29
+ // Consolidated email tool definitions (17 → 6)
30
+ const emailTools = [
31
+ {
32
+ name: 'search-emails',
33
+ description:
34
+ 'Search and list emails. With no query, lists recent emails (like list-emails). Supports search queries, KQL, delta sync, message-id lookup, and conversation listing.',
35
+ annotations: {
36
+ title: 'Search Emails',
37
+ readOnlyHint: true,
38
+ openWorldHint: false,
39
+ },
40
+ inputSchema: {
41
+ type: 'object',
42
+ properties: {
43
+ // Mode selectors (all optional — defaults to list mode)
44
+ deltaMode: {
45
+ type: 'boolean',
46
+ description:
47
+ 'Enable delta sync mode. Returns only changes since last sync. Use deltaToken for subsequent calls.',
48
+ },
49
+ internetMessageId: {
50
+ type: 'string',
51
+ description:
52
+ 'Look up email by Message-ID header (e.g. <abc123@example.com>). For threading/deduplication.',
53
+ },
54
+ conversationId: {
55
+ type: 'string',
56
+ description:
57
+ 'Get all messages in a conversation thread by conversationId.',
58
+ },
59
+ groupByConversation: {
60
+ type: 'boolean',
61
+ description:
62
+ 'List conversations (threads) grouped by conversationId instead of individual emails.',
63
+ },
64
+ // Search/list params
65
+ query: {
66
+ type: 'string',
67
+ description: 'Search query text. Omit for list mode.',
68
+ },
69
+ kqlQuery: {
70
+ type: 'string',
71
+ description:
72
+ 'Raw KQL (Keyword Query Language) query for advanced search. Bypasses other search params.',
73
+ },
74
+ folder: {
75
+ type: 'string',
76
+ description: "Email folder (default: 'inbox')",
77
+ },
78
+ from: {
79
+ type: 'string',
80
+ description: 'Filter by sender email/name',
81
+ },
82
+ to: {
83
+ type: 'string',
84
+ description: 'Filter by recipient email/name',
85
+ },
86
+ subject: {
87
+ type: 'string',
88
+ description: 'Filter by subject',
89
+ },
90
+ hasAttachments: {
91
+ type: 'boolean',
92
+ description: 'Filter to emails with attachments',
93
+ },
94
+ unreadOnly: {
95
+ type: 'boolean',
96
+ description: 'Filter to unread emails only',
97
+ },
98
+ receivedAfter: {
99
+ type: 'string',
100
+ description: 'Filter emails received after date (ISO 8601)',
101
+ },
102
+ receivedBefore: {
103
+ type: 'string',
104
+ description: 'Filter emails received before date (ISO 8601)',
105
+ },
106
+ searchAllFolders: {
107
+ type: 'boolean',
108
+ description: 'Search across all mail folders',
109
+ },
110
+ count: {
111
+ type: 'number',
112
+ description:
113
+ 'Number of results (list default: 25, search default: 10, max: 50)',
114
+ },
115
+ outputVerbosity: {
116
+ type: 'string',
117
+ enum: ['minimal', 'standard', 'full'],
118
+ description: 'Output detail level (default: standard)',
119
+ },
120
+ // Delta mode params
121
+ deltaToken: {
122
+ type: 'string',
123
+ description:
124
+ 'Token from previous delta call for incremental sync (deltaMode only)',
125
+ },
126
+ maxResults: {
127
+ type: 'number',
128
+ description:
129
+ 'Max results per page for delta sync (default: 100, max: 200)',
130
+ },
131
+ // Conversation params
132
+ includeHeaders: {
133
+ type: 'boolean',
134
+ description:
135
+ 'Include email headers for each message (conversationId only)',
136
+ },
137
+ },
138
+ required: [],
139
+ },
140
+ handler: async (args) => {
141
+ // Route to appropriate handler based on mode
142
+ if (args.deltaMode) {
143
+ return handleListEmailsDelta(args);
144
+ }
145
+ if (args.internetMessageId) {
146
+ return handleSearchByMessageId({
147
+ messageId: args.internetMessageId,
148
+ outputVerbosity: args.outputVerbosity,
149
+ });
150
+ }
151
+ if (args.conversationId) {
152
+ return handleGetConversation(args);
153
+ }
154
+ if (args.groupByConversation) {
155
+ return handleListConversations(args);
156
+ }
157
+ // If any search params provided, use search handler
158
+ if (
159
+ args.query ||
160
+ args.kqlQuery ||
161
+ args.from ||
162
+ args.to ||
163
+ args.subject ||
164
+ args.hasAttachments ||
165
+ args.unreadOnly ||
166
+ args.receivedAfter ||
167
+ args.receivedBefore ||
168
+ args.searchAllFolders
169
+ ) {
170
+ return handleSearchEmails(args);
171
+ }
172
+ // Default: list mode
173
+ return handleListEmails(args);
174
+ },
175
+ },
176
+ {
177
+ name: 'read-email',
178
+ description:
179
+ 'Read email content. Set headersMode=true for forensic headers (DKIM, SPF, Received, Message-ID).',
180
+ annotations: {
181
+ title: 'Read Email',
182
+ readOnlyHint: true,
183
+ openWorldHint: false,
184
+ },
185
+ inputSchema: {
186
+ type: 'object',
187
+ properties: {
188
+ id: {
189
+ type: 'string',
190
+ description: 'ID of the email to read',
191
+ },
192
+ headersMode: {
193
+ type: 'boolean',
194
+ description:
195
+ 'Return forensic headers instead of email content (default: false)',
196
+ },
197
+ includeHeaders: {
198
+ type: 'boolean',
199
+ description:
200
+ 'Include basic headers alongside email content (default: false)',
201
+ },
202
+ outputVerbosity: {
203
+ type: 'string',
204
+ enum: ['minimal', 'standard', 'full'],
205
+ description: 'Output detail level (default: standard)',
206
+ },
207
+ // Headers mode params
208
+ groupByType: {
209
+ type: 'boolean',
210
+ description:
211
+ 'Group headers by category (headersMode only, default: false)',
212
+ },
213
+ importantOnly: {
214
+ type: 'boolean',
215
+ description:
216
+ 'Show only important headers (headersMode only, default: false)',
217
+ },
218
+ raw: {
219
+ type: 'boolean',
220
+ description:
221
+ 'Return raw JSON instead of Markdown (headersMode only, default: false)',
222
+ },
223
+ },
224
+ required: ['id'],
225
+ },
226
+ handler: async (args) => {
227
+ if (args.headersMode) {
228
+ return handleGetEmailHeaders(args);
229
+ }
230
+ return handleReadEmail(args);
231
+ },
232
+ },
233
+ {
234
+ name: 'send-email',
235
+ description:
236
+ 'Compose and send an email. Use dryRun=true to preview without sending. Subject to rate limits and recipient allowlist when configured.',
237
+ annotations: {
238
+ title: 'Send Email',
239
+ readOnlyHint: false,
240
+ destructiveHint: true,
241
+ idempotentHint: false,
242
+ openWorldHint: true,
243
+ },
244
+ inputSchema: {
245
+ type: 'object',
246
+ properties: {
247
+ to: {
248
+ type: 'string',
249
+ description: 'Comma-separated recipient email addresses',
250
+ },
251
+ cc: {
252
+ type: 'string',
253
+ description: 'Comma-separated CC email addresses',
254
+ },
255
+ bcc: {
256
+ type: 'string',
257
+ description: 'Comma-separated BCC email addresses',
258
+ },
259
+ subject: {
260
+ type: 'string',
261
+ description: 'Email subject',
262
+ },
263
+ body: {
264
+ type: 'string',
265
+ description: 'Email body (plain text or HTML)',
266
+ },
267
+ importance: {
268
+ type: 'string',
269
+ enum: ['normal', 'high', 'low'],
270
+ description: 'Email importance (default: normal)',
271
+ },
272
+ saveToSentItems: {
273
+ type: 'boolean',
274
+ description: 'Save to sent items (default: true)',
275
+ },
276
+ dryRun: {
277
+ type: 'boolean',
278
+ description:
279
+ 'Preview email without sending (default: false). Returns composed email for review.',
280
+ },
281
+ },
282
+ required: ['to', 'subject', 'body'],
283
+ },
284
+ handler: handleSendEmail,
285
+ },
286
+ {
287
+ name: 'update-email',
288
+ description:
289
+ 'Update email state. action=mark-read/mark-unread changes read status. action=flag sets follow-up flag. action=unflag clears flag. action=complete marks flag as done.',
290
+ annotations: {
291
+ title: 'Update Email',
292
+ readOnlyHint: false,
293
+ destructiveHint: false,
294
+ idempotentHint: true,
295
+ openWorldHint: false,
296
+ },
297
+ inputSchema: {
298
+ type: 'object',
299
+ properties: {
300
+ action: {
301
+ type: 'string',
302
+ enum: ['mark-read', 'mark-unread', 'flag', 'unflag', 'complete'],
303
+ description: 'Action to perform (required)',
304
+ },
305
+ id: {
306
+ type: 'string',
307
+ description:
308
+ 'Single message ID (required for mark-read/mark-unread, or use instead of ids for flag actions)',
309
+ },
310
+ ids: {
311
+ type: 'array',
312
+ items: { type: 'string' },
313
+ description:
314
+ 'Array of message IDs for batch flag/unflag/complete operations',
315
+ },
316
+ // Flag params
317
+ dueDateTime: {
318
+ type: 'string',
319
+ description: 'Due date/time for follow-up, ISO 8601 (action=flag)',
320
+ },
321
+ startDateTime: {
322
+ type: 'string',
323
+ description: 'Start date/time for follow-up, ISO 8601 (action=flag)',
324
+ },
325
+ },
326
+ required: ['action'],
327
+ },
328
+ handler: async (args) => {
329
+ switch (args.action) {
330
+ case 'mark-read':
331
+ return handleMarkAsRead({ id: args.id, isRead: true });
332
+ case 'mark-unread':
333
+ return handleMarkAsRead({ id: args.id, isRead: false });
334
+ case 'flag':
335
+ return handleSetMessageFlag({
336
+ messageId: args.id,
337
+ messageIds: args.ids,
338
+ dueDateTime: args.dueDateTime,
339
+ startDateTime: args.startDateTime,
340
+ });
341
+ case 'unflag':
342
+ return handleClearMessageFlag({
343
+ messageId: args.id,
344
+ messageIds: args.ids,
345
+ markComplete: false,
346
+ });
347
+ case 'complete':
348
+ return handleClearMessageFlag({
349
+ messageId: args.id,
350
+ messageIds: args.ids,
351
+ markComplete: true,
352
+ });
353
+ default:
354
+ return {
355
+ content: [
356
+ {
357
+ type: 'text',
358
+ text: "Invalid action. Use 'mark-read', 'mark-unread', 'flag', 'unflag', or 'complete'.",
359
+ },
360
+ ],
361
+ };
362
+ }
363
+ },
364
+ },
365
+ {
366
+ name: 'attachments',
367
+ description:
368
+ 'Manage email attachments. action=list shows attachments for a message. action=view shows content/metadata. action=download saves to disk.',
369
+ annotations: {
370
+ title: 'Attachments',
371
+ readOnlyHint: false,
372
+ destructiveHint: false,
373
+ openWorldHint: false,
374
+ },
375
+ inputSchema: {
376
+ type: 'object',
377
+ properties: {
378
+ action: {
379
+ type: 'string',
380
+ enum: ['list', 'view', 'download'],
381
+ description: 'Action to perform (default: list)',
382
+ },
383
+ messageId: {
384
+ type: 'string',
385
+ description: 'Email message ID (required)',
386
+ },
387
+ attachmentId: {
388
+ type: 'string',
389
+ description: 'Attachment ID (action=view/download, required)',
390
+ },
391
+ savePath: {
392
+ type: 'string',
393
+ description:
394
+ 'Directory to save file (action=download, default: current directory)',
395
+ },
396
+ },
397
+ required: ['messageId'],
398
+ },
399
+ handler: async (args) => {
400
+ const action = args.action || 'list';
401
+ switch (action) {
402
+ case 'view':
403
+ return handleGetAttachmentContent(args);
404
+ case 'download':
405
+ return handleDownloadAttachment(args);
406
+ case 'list':
407
+ default:
408
+ return handleListAttachments(args);
409
+ }
410
+ },
411
+ },
412
+ {
413
+ name: 'export',
414
+ description:
415
+ 'Export emails. target=message exports one email. target=messages batch-exports. target=conversation exports a thread. target=mime gets raw MIME/EML content.',
416
+ annotations: {
417
+ title: 'Export Emails',
418
+ readOnlyHint: false,
419
+ destructiveHint: false,
420
+ openWorldHint: false,
421
+ },
422
+ inputSchema: {
423
+ type: 'object',
424
+ properties: {
425
+ target: {
426
+ type: 'string',
427
+ enum: ['message', 'messages', 'conversation', 'mime'],
428
+ description: 'Export target (default: message)',
429
+ },
430
+ // Single message export
431
+ id: {
432
+ type: 'string',
433
+ description: 'Email ID (target=message/mime, required)',
434
+ },
435
+ format: {
436
+ type: 'string',
437
+ enum: ['mime', 'eml', 'markdown', 'json', 'mbox', 'html'],
438
+ description:
439
+ 'Export format (target=message: mime/eml/markdown/json, target=conversation: eml/mbox/markdown/json/html)',
440
+ },
441
+ savePath: {
442
+ type: 'string',
443
+ description: 'File path or directory (target=message)',
444
+ },
445
+ includeAttachments: {
446
+ type: 'boolean',
447
+ description:
448
+ 'Include attachments (default: true for single, false for batch)',
449
+ },
450
+ // Batch export
451
+ emailIds: {
452
+ type: 'array',
453
+ items: { type: 'string' },
454
+ description: 'Email IDs to export (target=messages)',
455
+ },
456
+ searchQuery: {
457
+ type: 'object',
458
+ properties: {
459
+ folder: { type: 'string' },
460
+ from: { type: 'string' },
461
+ subject: { type: 'string' },
462
+ receivedAfter: { type: 'string' },
463
+ receivedBefore: { type: 'string' },
464
+ maxResults: { type: 'number' },
465
+ },
466
+ description:
467
+ 'Search query to find emails (target=messages, alternative to emailIds)',
468
+ },
469
+ outputDir: {
470
+ type: 'string',
471
+ description:
472
+ 'Output directory (target=messages/conversation, required)',
473
+ },
474
+ // Conversation export
475
+ conversationId: {
476
+ type: 'string',
477
+ description: 'Conversation ID (target=conversation, required)',
478
+ },
479
+ order: {
480
+ type: 'string',
481
+ enum: ['chronological', 'reverse'],
482
+ description:
483
+ 'Message order (target=conversation, default: chronological)',
484
+ },
485
+ // MIME params
486
+ headersOnly: {
487
+ type: 'boolean',
488
+ description: 'MIME headers only, no body (target=mime)',
489
+ },
490
+ base64: {
491
+ type: 'boolean',
492
+ description: 'Return base64 encoded (target=mime)',
493
+ },
494
+ maxSize: {
495
+ type: 'number',
496
+ description: 'Max content size in bytes (target=mime, default: 1MB)',
497
+ },
498
+ },
499
+ required: [],
500
+ },
501
+ handler: async (args) => {
502
+ const target = args.target || 'message';
503
+ switch (target) {
504
+ case 'messages':
505
+ return handleBatchExportEmails(args);
506
+ case 'conversation':
507
+ return handleExportConversation(args);
508
+ case 'mime':
509
+ return handleGetMimeContent(args);
510
+ case 'message':
511
+ default:
512
+ return handleExportEmail(args);
513
+ }
514
+ },
515
+ },
516
+ ];
517
+
518
+ module.exports = {
519
+ emailTools,
520
+ handleListEmails,
521
+ handleSearchEmails,
522
+ handleSearchByMessageId,
523
+ handleReadEmail,
524
+ handleSendEmail,
525
+ handleMarkAsRead,
526
+ handleListAttachments,
527
+ handleDownloadAttachment,
528
+ handleGetAttachmentContent,
529
+ handleExportEmail,
530
+ handleBatchExportEmails,
531
+ handleListEmailsDelta,
532
+ handleGetEmailHeaders,
533
+ handleGetMimeContent,
534
+ handleListConversations,
535
+ handleGetConversation,
536
+ handleExportConversation,
537
+ };
package/email/list.js ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * List emails functionality
3
+ *
4
+ * Token-efficient implementation with outputVerbosity support and Markdown formatting.
5
+ */
6
+ const config = require('../config');
7
+ const {
8
+ callGraphAPI: _callGraphAPI,
9
+ callGraphAPIPaginated,
10
+ } = require('../utils/graph-api');
11
+ const { ensureAuthenticated } = require('../auth');
12
+ const { resolveFolderPath } = require('./folder-utils');
13
+ const {
14
+ formatEmailList,
15
+ VERBOSITY,
16
+ DEFAULT_LIMITS,
17
+ } = require('../utils/response-formatter');
18
+ const { getEmailFields } = require('../utils/field-presets');
19
+
20
+ /**
21
+ * Maps verbosity level to field preset
22
+ * @param {string} verbosity - minimal, standard, or full
23
+ * @returns {string} - field preset name
24
+ */
25
+ function getFieldPresetForVerbosity(verbosity) {
26
+ switch (verbosity) {
27
+ case VERBOSITY.MINIMAL:
28
+ return 'list'; // id, subject, from, receivedDateTime, isRead
29
+ case VERBOSITY.FULL:
30
+ return 'search'; // Includes toRecipients, bodyPreview, hasAttachments, importance
31
+ case VERBOSITY.STANDARD:
32
+ default:
33
+ return 'list'; // Standard uses list preset but formats with more detail
34
+ }
35
+ }
36
+
37
+ /**
38
+ * List emails handler
39
+ * @param {object} args - Tool arguments
40
+ * @param {string} [args.folder] - Folder to list (default: inbox)
41
+ * @param {number} [args.count] - Number of emails (default: 25, max: 50)
42
+ * @param {string} [args.outputVerbosity] - minimal, standard, or full (default: standard)
43
+ * @returns {object} - MCP response with Markdown formatted content
44
+ */
45
+ async function handleListEmails(args) {
46
+ const folder = args.folder || 'inbox';
47
+ const requestedCount = args.count || DEFAULT_LIMITS.listEmails; // Default 25 (was 10)
48
+ const verbosity = args.outputVerbosity || VERBOSITY.STANDARD;
49
+
50
+ try {
51
+ // Get access token
52
+ const accessToken = await ensureAuthenticated();
53
+
54
+ // Resolve the folder path
55
+ const endpoint = await resolveFolderPath(accessToken, folder);
56
+
57
+ // Select fields based on verbosity level
58
+ const fieldPreset = getFieldPresetForVerbosity(verbosity);
59
+ const selectFields = getEmailFields(fieldPreset);
60
+
61
+ // Add query parameters
62
+ const queryParams = {
63
+ $top: Math.min(config.MAX_RESULT_COUNT, requestedCount),
64
+ $orderby: 'receivedDateTime desc',
65
+ $select: selectFields,
66
+ };
67
+
68
+ // Make API call with pagination support
69
+ const response = await callGraphAPIPaginated(
70
+ accessToken,
71
+ 'GET',
72
+ endpoint,
73
+ queryParams,
74
+ requestedCount
75
+ );
76
+
77
+ if (!response.value || response.value.length === 0) {
78
+ return {
79
+ content: [
80
+ {
81
+ type: 'text',
82
+ text: `No emails found in ${folder}.`,
83
+ },
84
+ ],
85
+ };
86
+ }
87
+
88
+ // Build response metadata
89
+ const meta = {
90
+ returned: response.value.length,
91
+ totalAvailable: response['@odata.count'] || null,
92
+ hasMore: !!response['@odata.nextLink'],
93
+ verbosity: verbosity,
94
+ };
95
+
96
+ // Format results using response-formatter (returns Markdown)
97
+ const formattedOutput = formatEmailList(
98
+ response.value,
99
+ folder,
100
+ verbosity,
101
+ meta
102
+ );
103
+
104
+ return {
105
+ content: [
106
+ {
107
+ type: 'text',
108
+ text: formattedOutput,
109
+ },
110
+ ],
111
+ _meta: meta,
112
+ };
113
+ } catch (error) {
114
+ if (error.message === 'Authentication required') {
115
+ return {
116
+ content: [
117
+ {
118
+ type: 'text',
119
+ text: "Authentication required. Please use the 'authenticate' tool first.",
120
+ },
121
+ ],
122
+ };
123
+ }
124
+
125
+ return {
126
+ content: [
127
+ {
128
+ type: 'text',
129
+ text: `Error listing emails: ${error.message}`,
130
+ },
131
+ ],
132
+ };
133
+ }
134
+ }
135
+
136
+ module.exports = handleListEmails;