@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,192 @@
1
+ /**
2
+ * Email folder utilities
3
+ */
4
+ const { callGraphAPI } = require('../utils/graph-api');
5
+
6
+ /**
7
+ * Cache of folder information to reduce API calls
8
+ * Format: { userId: { folderName: { id, path } } }
9
+ */
10
+ const _folderCache = {};
11
+
12
+ /**
13
+ * Well-known folder names and their endpoints
14
+ */
15
+ const WELL_KNOWN_FOLDERS = {
16
+ inbox: 'me/mailFolders/inbox/messages',
17
+ drafts: 'me/mailFolders/drafts/messages',
18
+ sent: 'me/mailFolders/sentItems/messages',
19
+ deleted: 'me/mailFolders/deletedItems/messages',
20
+ junk: 'me/mailFolders/junkemail/messages',
21
+ archive: 'me/mailFolders/archive/messages',
22
+ };
23
+
24
+ /**
25
+ * Resolve a folder name to its endpoint path
26
+ * @param {string} accessToken - Access token
27
+ * @param {string} folderName - Folder name to resolve
28
+ * @returns {Promise<string>} - Resolved endpoint path
29
+ */
30
+ async function resolveFolderPath(accessToken, folderName) {
31
+ // Default to inbox if no folder specified
32
+ if (!folderName) {
33
+ return WELL_KNOWN_FOLDERS['inbox'];
34
+ }
35
+
36
+ // Check if it's a well-known folder (case-insensitive)
37
+ const lowerFolderName = folderName.toLowerCase();
38
+ if (WELL_KNOWN_FOLDERS[lowerFolderName]) {
39
+ console.error(`Using well-known folder path for "${folderName}"`);
40
+ return WELL_KNOWN_FOLDERS[lowerFolderName];
41
+ }
42
+
43
+ try {
44
+ // Try to find the folder by name
45
+ const folderId = await getFolderIdByName(accessToken, folderName);
46
+ if (folderId) {
47
+ const path = `me/mailFolders/${folderId}/messages`;
48
+ console.error(`Resolved folder "${folderName}" to path: ${path}`);
49
+ return path;
50
+ }
51
+
52
+ // If not found, throw error instead of silently falling back
53
+ throw new Error(
54
+ `Folder "${folderName}" not found. Use the folders tool (action=list) to see available folders.`
55
+ );
56
+ } catch (error) {
57
+ if (error.message.includes('not found')) {
58
+ throw error;
59
+ }
60
+ throw new Error(
61
+ `Error resolving folder "${folderName}": ${error.message}. Use the folders tool (action=list) to see available folders.`,
62
+ { cause: error }
63
+ );
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Get the ID of a mail folder by its name
69
+ * @param {string} accessToken - Access token
70
+ * @param {string} folderName - Name of the folder to find
71
+ * @returns {Promise<string|null>} - Folder ID or null if not found
72
+ */
73
+ async function getFolderIdByName(accessToken, folderName) {
74
+ try {
75
+ // First try with exact match filter
76
+ console.error(`Looking for folder with name "${folderName}"`);
77
+ const response = await callGraphAPI(
78
+ accessToken,
79
+ 'GET',
80
+ 'me/mailFolders',
81
+ null,
82
+ { $filter: `displayName eq '${folderName}'` }
83
+ );
84
+
85
+ if (response.value && response.value.length > 0) {
86
+ console.error(
87
+ `Found folder "${folderName}" with ID: ${response.value[0].id}`
88
+ );
89
+ return response.value[0].id;
90
+ }
91
+
92
+ // If exact match fails, try to get all folders and do a case-insensitive comparison
93
+ console.error(
94
+ `No exact match found for "${folderName}", trying case-insensitive search`
95
+ );
96
+ const allFoldersResponse = await callGraphAPI(
97
+ accessToken,
98
+ 'GET',
99
+ 'me/mailFolders',
100
+ null,
101
+ { $top: 100 }
102
+ );
103
+
104
+ if (allFoldersResponse.value) {
105
+ const lowerFolderName = folderName.toLowerCase();
106
+ const matchingFolder = allFoldersResponse.value.find(
107
+ (folder) => folder.displayName.toLowerCase() === lowerFolderName
108
+ );
109
+
110
+ if (matchingFolder) {
111
+ console.error(
112
+ `Found case-insensitive match for "${folderName}" with ID: ${matchingFolder.id}`
113
+ );
114
+ return matchingFolder.id;
115
+ }
116
+ }
117
+
118
+ console.error(`No folder found matching "${folderName}"`);
119
+ return null;
120
+ } catch (error) {
121
+ console.error(`Error finding folder "${folderName}": ${error.message}`);
122
+ return null;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Get all mail folders
128
+ * @param {string} accessToken - Access token
129
+ * @returns {Promise<Array>} - Array of folder objects
130
+ */
131
+ async function getAllFolders(accessToken) {
132
+ try {
133
+ // Get top-level folders
134
+ const response = await callGraphAPI(
135
+ accessToken,
136
+ 'GET',
137
+ 'me/mailFolders',
138
+ null,
139
+ {
140
+ $top: 100,
141
+ $select:
142
+ 'id,displayName,parentFolderId,childFolderCount,totalItemCount,unreadItemCount',
143
+ }
144
+ );
145
+
146
+ if (!response.value) {
147
+ return [];
148
+ }
149
+
150
+ // Get child folders for folders with children
151
+ const foldersWithChildren = response.value.filter(
152
+ (f) => f.childFolderCount > 0
153
+ );
154
+
155
+ const childFolderPromises = foldersWithChildren.map(async (folder) => {
156
+ try {
157
+ const childResponse = await callGraphAPI(
158
+ accessToken,
159
+ 'GET',
160
+ `me/mailFolders/${folder.id}/childFolders`,
161
+ null,
162
+ {
163
+ $select:
164
+ 'id,displayName,parentFolderId,childFolderCount,totalItemCount,unreadItemCount',
165
+ }
166
+ );
167
+
168
+ return childResponse.value || [];
169
+ } catch (error) {
170
+ console.error(
171
+ `Error getting child folders for "${folder.displayName}": ${error.message}`
172
+ );
173
+ return [];
174
+ }
175
+ });
176
+
177
+ const childFolders = await Promise.all(childFolderPromises);
178
+
179
+ // Combine top-level folders and all child folders
180
+ return [...response.value, ...childFolders.flat()];
181
+ } catch (error) {
182
+ console.error(`Error getting all folders: ${error.message}`);
183
+ return [];
184
+ }
185
+ }
186
+
187
+ module.exports = {
188
+ WELL_KNOWN_FOLDERS,
189
+ resolveFolderPath,
190
+ getFolderIdByName,
191
+ getAllFolders,
192
+ };
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Email headers functionality
3
+ *
4
+ * Retrieves email headers for forensics, spam analysis, delivery troubleshooting,
5
+ * and threading reconstruction.
6
+ */
7
+ const { callGraphAPI } = require('../utils/graph-api');
8
+ const { ensureAuthenticated } = require('../auth');
9
+
10
+ /**
11
+ * Important headers to highlight (in order of relevance)
12
+ */
13
+ const IMPORTANT_HEADERS = [
14
+ // Threading headers
15
+ 'Message-ID',
16
+ 'In-Reply-To',
17
+ 'References',
18
+ // Authentication headers
19
+ 'Authentication-Results',
20
+ 'DKIM-Signature',
21
+ 'ARC-Authentication-Results',
22
+ // Delivery chain
23
+ 'Received',
24
+ 'Received-SPF',
25
+ // Spam/filtering
26
+ 'X-MS-Exchange-Organization-SCL',
27
+ 'X-MS-Exchange-Organization-AuthSource',
28
+ 'X-Forefront-Antispam-Report',
29
+ 'X-Microsoft-Antispam',
30
+ // Content
31
+ 'Content-Type',
32
+ 'MIME-Version',
33
+ // Custom headers
34
+ 'X-Mailer',
35
+ 'X-Originating-IP',
36
+ 'X-Priority',
37
+ ];
38
+
39
+ /**
40
+ * Format headers as Markdown
41
+ * @param {Array} headers - Array of {name, value} header objects
42
+ * @param {object} options - Formatting options
43
+ * @returns {string} - Markdown formatted headers
44
+ */
45
+ function formatHeaders(headers, options = {}) {
46
+ const { groupByType = false, includeAll = true } = options;
47
+
48
+ if (!headers || headers.length === 0) {
49
+ return '*No headers available*';
50
+ }
51
+
52
+ let output = [];
53
+
54
+ if (groupByType) {
55
+ // Group headers by category
56
+ const groups = {
57
+ Threading: [],
58
+ Authentication: [],
59
+ Delivery: [],
60
+ 'Spam/Security': [],
61
+ Content: [],
62
+ Other: [],
63
+ };
64
+
65
+ headers.forEach((h) => {
66
+ const name = h.name;
67
+ if (['Message-ID', 'In-Reply-To', 'References'].includes(name)) {
68
+ groups['Threading'].push(h);
69
+ } else if (
70
+ [
71
+ 'Authentication-Results',
72
+ 'DKIM-Signature',
73
+ 'ARC-Authentication-Results',
74
+ 'Received-SPF',
75
+ ].includes(name)
76
+ ) {
77
+ groups['Authentication'].push(h);
78
+ } else if (name === 'Received' || name.startsWith('X-MS-Exchange')) {
79
+ groups['Delivery'].push(h);
80
+ } else if (
81
+ name.includes('Antispam') ||
82
+ name.includes('SCL') ||
83
+ name === 'X-Spam-Status'
84
+ ) {
85
+ groups['Spam/Security'].push(h);
86
+ } else if (
87
+ ['Content-Type', 'MIME-Version', 'Content-Transfer-Encoding'].includes(
88
+ name
89
+ )
90
+ ) {
91
+ groups['Content'].push(h);
92
+ } else {
93
+ groups['Other'].push(h);
94
+ }
95
+ });
96
+
97
+ for (const [groupName, groupHeaders] of Object.entries(groups)) {
98
+ if (groupHeaders.length > 0) {
99
+ output.push(`\n### ${groupName}`);
100
+ groupHeaders.forEach((h) => {
101
+ // Truncate very long values
102
+ const value =
103
+ h.value.length > 500 ? h.value.substring(0, 500) + '...' : h.value;
104
+ output.push(`**${h.name}**: \`${value}\``);
105
+ });
106
+ }
107
+ }
108
+ } else {
109
+ // Simple list format
110
+ const importantSet = new Set(IMPORTANT_HEADERS);
111
+ const importantHeaders = [];
112
+ const otherHeaders = [];
113
+
114
+ headers.forEach((h) => {
115
+ if (importantSet.has(h.name)) {
116
+ importantHeaders.push(h);
117
+ } else {
118
+ otherHeaders.push(h);
119
+ }
120
+ });
121
+
122
+ if (importantHeaders.length > 0) {
123
+ output.push('## Key Headers\n');
124
+ importantHeaders.forEach((h) => {
125
+ const value =
126
+ h.value.length > 300 ? h.value.substring(0, 300) + '...' : h.value;
127
+ output.push(`**${h.name}**:`);
128
+ output.push('```');
129
+ output.push(value);
130
+ output.push('```\n');
131
+ });
132
+ }
133
+
134
+ if (includeAll && otherHeaders.length > 0) {
135
+ output.push('\n## All Other Headers\n');
136
+ otherHeaders.forEach((h) => {
137
+ const value =
138
+ h.value.length > 200 ? h.value.substring(0, 200) + '...' : h.value;
139
+ output.push(`- **${h.name}**: \`${value}\``);
140
+ });
141
+ }
142
+ }
143
+
144
+ return output.join('\n');
145
+ }
146
+
147
+ /**
148
+ * Get email headers handler
149
+ * @param {object} args - Tool arguments
150
+ * @param {string} args.id - Email ID (required)
151
+ * @param {boolean} [args.groupByType] - Group headers by category (default: false)
152
+ * @param {boolean} [args.importantOnly] - Show only important headers (default: false)
153
+ * @param {boolean} [args.raw] - Return raw JSON instead of formatted (default: false)
154
+ * @returns {object} - MCP response with headers
155
+ */
156
+ async function handleGetEmailHeaders(args) {
157
+ const emailId = args.id;
158
+ const groupByType = args.groupByType || false;
159
+ const importantOnly = args.importantOnly || false;
160
+ const raw = args.raw || false;
161
+
162
+ if (!emailId) {
163
+ return {
164
+ content: [
165
+ {
166
+ type: 'text',
167
+ text: 'Email ID is required.',
168
+ },
169
+ ],
170
+ };
171
+ }
172
+
173
+ try {
174
+ // Get access token
175
+ const accessToken = await ensureAuthenticated();
176
+
177
+ // Request headers plus threading metadata
178
+ const selectFields = [
179
+ 'id',
180
+ 'subject',
181
+ 'from',
182
+ 'internetMessageHeaders',
183
+ 'internetMessageId',
184
+ 'conversationId',
185
+ 'conversationIndex',
186
+ 'receivedDateTime',
187
+ 'sentDateTime',
188
+ ].join(',');
189
+
190
+ const endpoint = `me/messages/${emailId}`;
191
+ const queryParams = {
192
+ $select: selectFields,
193
+ };
194
+
195
+ try {
196
+ const email = await callGraphAPI(
197
+ accessToken,
198
+ 'GET',
199
+ endpoint,
200
+ null,
201
+ queryParams
202
+ );
203
+
204
+ if (!email) {
205
+ return {
206
+ content: [
207
+ {
208
+ type: 'text',
209
+ text: `Email with ID ${emailId} not found.`,
210
+ },
211
+ ],
212
+ };
213
+ }
214
+
215
+ const headers = email.internetMessageHeaders || [];
216
+
217
+ // Filter to important headers if requested
218
+ let filteredHeaders = headers;
219
+ if (importantOnly) {
220
+ const importantSet = new Set(IMPORTANT_HEADERS);
221
+ filteredHeaders = headers.filter((h) => importantSet.has(h.name));
222
+ }
223
+
224
+ // Return raw JSON if requested
225
+ if (raw) {
226
+ return {
227
+ content: [
228
+ {
229
+ type: 'text',
230
+ text: JSON.stringify(
231
+ {
232
+ id: email.id,
233
+ subject: email.subject,
234
+ internetMessageId: email.internetMessageId,
235
+ conversationId: email.conversationId,
236
+ headers: filteredHeaders,
237
+ },
238
+ null,
239
+ 2
240
+ ),
241
+ },
242
+ ],
243
+ _meta: {
244
+ emailId: email.id,
245
+ headerCount: filteredHeaders.length,
246
+ format: 'json',
247
+ },
248
+ };
249
+ }
250
+
251
+ // Build formatted output
252
+ let output = [];
253
+ output.push(`# Email Headers\n`);
254
+ output.push(`**Subject**: ${email.subject || '(no subject)'}`);
255
+ output.push(
256
+ `**From**: ${email.from?.emailAddress?.address || 'unknown'}`
257
+ );
258
+ output.push(`**Received**: ${email.receivedDateTime || 'unknown'}`);
259
+ output.push(
260
+ `**Message-ID**: \`${email.internetMessageId || 'not available'}\``
261
+ );
262
+ output.push(
263
+ `**Conversation-ID**: \`${email.conversationId || 'not available'}\``
264
+ );
265
+ output.push(`\n---\n`);
266
+ output.push(`**Total Headers**: ${headers.length}`);
267
+ if (importantOnly) {
268
+ output.push(` (showing ${filteredHeaders.length} important headers)`);
269
+ }
270
+ output.push('\n');
271
+
272
+ output.push(
273
+ formatHeaders(filteredHeaders, {
274
+ groupByType: groupByType,
275
+ includeAll: !importantOnly,
276
+ })
277
+ );
278
+
279
+ return {
280
+ content: [
281
+ {
282
+ type: 'text',
283
+ text: output.join('\n'),
284
+ },
285
+ ],
286
+ _meta: {
287
+ emailId: email.id,
288
+ internetMessageId: email.internetMessageId,
289
+ conversationId: email.conversationId,
290
+ headerCount: headers.length,
291
+ displayedHeaders: filteredHeaders.length,
292
+ },
293
+ };
294
+ } catch (error) {
295
+ console.error(`Error getting email headers: ${error.message}`);
296
+
297
+ if (error.message.includes("doesn't belong to the targeted mailbox")) {
298
+ return {
299
+ content: [
300
+ {
301
+ type: 'text',
302
+ text: `The email ID seems invalid or doesn't belong to your mailbox.`,
303
+ },
304
+ ],
305
+ };
306
+ }
307
+
308
+ return {
309
+ content: [
310
+ {
311
+ type: 'text',
312
+ text: `Failed to get email headers: ${error.message}`,
313
+ },
314
+ ],
315
+ };
316
+ }
317
+ } catch (error) {
318
+ if (error.message === 'Authentication required') {
319
+ return {
320
+ content: [
321
+ {
322
+ type: 'text',
323
+ text: "Authentication required. Please use the 'authenticate' tool first.",
324
+ },
325
+ ],
326
+ };
327
+ }
328
+
329
+ return {
330
+ content: [
331
+ {
332
+ type: 'text',
333
+ text: `Error accessing email: ${error.message}`,
334
+ },
335
+ ],
336
+ };
337
+ }
338
+ }
339
+
340
+ module.exports = {
341
+ handleGetEmailHeaders,
342
+ formatHeaders,
343
+ IMPORTANT_HEADERS,
344
+ };