@tgai96/outlook-mcp 1.0.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.
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Microsoft Graph API helper functions
3
+ */
4
+ const https = require('https');
5
+ const config = require('../config');
6
+ const mockData = require('./mock-data');
7
+
8
+ /**
9
+ * Makes a request to the Microsoft Graph API
10
+ * @param {string} accessToken - The access token for authentication
11
+ * @param {string} method - HTTP method (GET, POST, etc.)
12
+ * @param {string} path - API endpoint path
13
+ * @param {object} data - Data to send for POST/PUT requests
14
+ * @param {object} queryParams - Query parameters
15
+ * @returns {Promise<object>} - The API response
16
+ */
17
+ async function callGraphAPI(accessToken, method, path, data = null, queryParams = {}) {
18
+ // For test tokens, we'll simulate the API call
19
+ if (config.USE_TEST_MODE && accessToken.startsWith('test_access_token_')) {
20
+ console.error(`TEST MODE: Simulating ${method} ${path} API call`);
21
+ return mockData.simulateGraphAPIResponse(method, path, data, queryParams);
22
+ }
23
+
24
+ try {
25
+ console.error(`Making real API call: ${method} ${path}`);
26
+
27
+ // Check if path already contains the full URL (from nextLink)
28
+ let finalUrl;
29
+ if (path.startsWith('http://') || path.startsWith('https://')) {
30
+ // Path is already a full URL (from pagination nextLink)
31
+ finalUrl = path;
32
+ console.error(`Using full URL from nextLink: ${finalUrl}`);
33
+ } else {
34
+ // Build URL from path and queryParams
35
+ // Encode path segments properly
36
+ const encodedPath = path.split('/')
37
+ .map(segment => encodeURIComponent(segment))
38
+ .join('/');
39
+
40
+ // Build query string from parameters with special handling for OData filters
41
+ let queryString = '';
42
+ if (Object.keys(queryParams).length > 0) {
43
+ // Handle $filter parameter specially to ensure proper URI encoding
44
+ const filter = queryParams.$filter;
45
+ if (filter) {
46
+ delete queryParams.$filter; // Remove from regular params
47
+ }
48
+
49
+ // Build query string with proper encoding for regular params
50
+ const params = new URLSearchParams();
51
+ for (const [key, value] of Object.entries(queryParams)) {
52
+ params.append(key, value);
53
+ }
54
+
55
+ queryString = params.toString();
56
+
57
+ // Add filter parameter separately with proper encoding
58
+ if (filter) {
59
+ if (queryString) {
60
+ queryString += `&$filter=${encodeURIComponent(filter)}`;
61
+ } else {
62
+ queryString = `$filter=${encodeURIComponent(filter)}`;
63
+ }
64
+ }
65
+
66
+ if (queryString) {
67
+ queryString = '?' + queryString;
68
+ }
69
+
70
+ console.error(`Query string: ${queryString}`);
71
+ }
72
+
73
+ finalUrl = `${config.GRAPH_API_ENDPOINT}${encodedPath}${queryString}`;
74
+ console.error(`Full URL: ${finalUrl}`);
75
+ }
76
+
77
+ return new Promise((resolve, reject) => {
78
+ const options = {
79
+ method: method,
80
+ headers: {
81
+ 'Authorization': `Bearer ${accessToken}`,
82
+ 'Content-Type': 'application/json'
83
+ }
84
+ };
85
+
86
+ const req = https.request(finalUrl, options, (res) => {
87
+ let responseData = '';
88
+
89
+ res.on('data', (chunk) => {
90
+ responseData += chunk;
91
+ });
92
+
93
+ res.on('end', () => {
94
+ if (res.statusCode >= 200 && res.statusCode < 300) {
95
+ try {
96
+ responseData = responseData ? responseData : '{}';
97
+ const jsonResponse = JSON.parse(responseData);
98
+ resolve(jsonResponse);
99
+ } catch (error) {
100
+ reject(new Error(`Error parsing API response: ${error.message}`));
101
+ }
102
+ } else if (res.statusCode === 401) {
103
+ // Token expired or invalid
104
+ reject(new Error('UNAUTHORIZED'));
105
+ } else {
106
+ reject(new Error(`API call failed with status ${res.statusCode}: ${responseData}`));
107
+ }
108
+ });
109
+ });
110
+
111
+ req.on('error', (error) => {
112
+ reject(new Error(`Network error during API call: ${error.message}`));
113
+ });
114
+
115
+ if (data && (method === 'POST' || method === 'PATCH' || method === 'PUT')) {
116
+ req.write(JSON.stringify(data));
117
+ }
118
+
119
+ req.end();
120
+ });
121
+ } catch (error) {
122
+ console.error('Error calling Graph API:', error);
123
+ throw error;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Calls Graph API with pagination support to retrieve all results up to maxCount
129
+ * @param {string} accessToken - The access token for authentication
130
+ * @param {string} method - HTTP method (GET only for pagination)
131
+ * @param {string} path - API endpoint path
132
+ * @param {object} queryParams - Initial query parameters
133
+ * @param {number} maxCount - Maximum number of items to retrieve (0 = all)
134
+ * @returns {Promise<object>} - Combined API response with all items
135
+ */
136
+ async function callGraphAPIPaginated(accessToken, method, path, queryParams = {}, maxCount = 0) {
137
+ if (method !== 'GET') {
138
+ throw new Error('Pagination only supports GET requests');
139
+ }
140
+
141
+ const allItems = [];
142
+ let nextLink = null;
143
+ let currentUrl = path;
144
+ let currentParams = queryParams;
145
+
146
+ try {
147
+ do {
148
+ // Make API call
149
+ const response = await callGraphAPI(accessToken, method, currentUrl, null, currentParams);
150
+
151
+ // Add items from this page
152
+ if (response.value && Array.isArray(response.value)) {
153
+ allItems.push(...response.value);
154
+ console.error(`Pagination: Retrieved ${response.value.length} items, total so far: ${allItems.length}`);
155
+ }
156
+
157
+ // Check if we've reached the desired count
158
+ if (maxCount > 0 && allItems.length >= maxCount) {
159
+ console.error(`Pagination: Reached max count of ${maxCount}, stopping`);
160
+ break;
161
+ }
162
+
163
+ // Get next page URL
164
+ nextLink = response['@odata.nextLink'];
165
+
166
+ if (nextLink) {
167
+ // Pass the full nextLink URL directly to callGraphAPI
168
+ currentUrl = nextLink;
169
+ currentParams = {}; // nextLink already contains all params
170
+ console.error(`Pagination: Following nextLink, ${allItems.length} items so far`);
171
+ }
172
+ } while (nextLink);
173
+
174
+ // Trim to exact count if needed
175
+ const finalItems = maxCount > 0 ? allItems.slice(0, maxCount) : allItems;
176
+
177
+ console.error(`Pagination complete: Retrieved ${finalItems.length} total items`);
178
+
179
+ return {
180
+ value: finalItems,
181
+ '@odata.count': finalItems.length
182
+ };
183
+ } catch (error) {
184
+ console.error('Error during pagination:', error);
185
+ throw error;
186
+ }
187
+ }
188
+
189
+ module.exports = {
190
+ callGraphAPI,
191
+ callGraphAPIPaginated
192
+ };
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Mock data functions for test mode
3
+ */
4
+
5
+ /**
6
+ * Simulates Microsoft Graph API responses for testing
7
+ * @param {string} method - HTTP method
8
+ * @param {string} path - API path
9
+ * @param {object} data - Request data
10
+ * @param {object} queryParams - Query parameters
11
+ * @returns {object} - Simulated API response
12
+ */
13
+ function simulateGraphAPIResponse(method, path, data, queryParams) {
14
+ console.error(`Simulating response for: ${method} ${path}`);
15
+
16
+ if (method === 'GET') {
17
+ if (path.includes('messages') && !path.includes('sendMail')) {
18
+ // Simulate a successful email list/search response
19
+ if (path.includes('/messages/')) {
20
+ // Single email response
21
+ return {
22
+ id: "simulated-email-id",
23
+ subject: "Simulated Email Subject",
24
+ from: {
25
+ emailAddress: {
26
+ name: "Simulated Sender",
27
+ address: "sender@example.com"
28
+ }
29
+ },
30
+ toRecipients: [{
31
+ emailAddress: {
32
+ name: "Recipient Name",
33
+ address: "recipient@example.com"
34
+ }
35
+ }],
36
+ ccRecipients: [],
37
+ bccRecipients: [],
38
+ receivedDateTime: new Date().toISOString(),
39
+ bodyPreview: "This is a simulated email preview...",
40
+ body: {
41
+ contentType: "text",
42
+ content: "This is the full content of the simulated email. Since we can't connect to the real Microsoft Graph API, we're returning this placeholder content instead."
43
+ },
44
+ hasAttachments: false,
45
+ importance: "normal",
46
+ isRead: false,
47
+ internetMessageHeaders: []
48
+ };
49
+ } else {
50
+ // Email list response
51
+ return {
52
+ value: [
53
+ {
54
+ id: "simulated-email-1",
55
+ subject: "Important Meeting Tomorrow",
56
+ from: {
57
+ emailAddress: {
58
+ name: "John Doe",
59
+ address: "john@example.com"
60
+ }
61
+ },
62
+ toRecipients: [{
63
+ emailAddress: {
64
+ name: "You",
65
+ address: "you@example.com"
66
+ }
67
+ }],
68
+ ccRecipients: [],
69
+ receivedDateTime: new Date().toISOString(),
70
+ bodyPreview: "Let's discuss the project status...",
71
+ hasAttachments: false,
72
+ importance: "high",
73
+ isRead: false
74
+ },
75
+ {
76
+ id: "simulated-email-2",
77
+ subject: "Weekly Report",
78
+ from: {
79
+ emailAddress: {
80
+ name: "Jane Smith",
81
+ address: "jane@example.com"
82
+ }
83
+ },
84
+ toRecipients: [{
85
+ emailAddress: {
86
+ name: "You",
87
+ address: "you@example.com"
88
+ }
89
+ }],
90
+ ccRecipients: [],
91
+ receivedDateTime: new Date(Date.now() - 86400000).toISOString(), // Yesterday
92
+ bodyPreview: "Please find attached the weekly report...",
93
+ hasAttachments: true,
94
+ importance: "normal",
95
+ isRead: true
96
+ },
97
+ {
98
+ id: "simulated-email-3",
99
+ subject: "Question about the project",
100
+ from: {
101
+ emailAddress: {
102
+ name: "Bob Johnson",
103
+ address: "bob@example.com"
104
+ }
105
+ },
106
+ toRecipients: [{
107
+ emailAddress: {
108
+ name: "You",
109
+ address: "you@example.com"
110
+ }
111
+ }],
112
+ ccRecipients: [],
113
+ receivedDateTime: new Date(Date.now() - 172800000).toISOString(), // 2 days ago
114
+ bodyPreview: "I had a question about the timeline...",
115
+ hasAttachments: false,
116
+ importance: "normal",
117
+ isRead: false
118
+ }
119
+ ]
120
+ };
121
+ }
122
+ } else if (path.includes('mailFolders')) {
123
+ // Simulate a mail folders response
124
+ return {
125
+ value: [
126
+ { id: "inbox", displayName: "Inbox" },
127
+ { id: "drafts", displayName: "Drafts" },
128
+ { id: "sentItems", displayName: "Sent Items" },
129
+ { id: "deleteditems", displayName: "Deleted Items" }
130
+ ]
131
+ };
132
+ }
133
+ } else if (method === 'POST' && path.includes('sendMail')) {
134
+ // Simulate a successful email send
135
+ return {};
136
+ }
137
+
138
+ // If we get here, we don't have a simulation for this endpoint
139
+ console.error(`No simulation available for: ${method} ${path}`);
140
+ return {};
141
+ }
142
+
143
+ module.exports = {
144
+ simulateGraphAPIResponse
145
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * OData helper functions for Microsoft Graph API
3
+ */
4
+
5
+ /**
6
+ * Escapes a string for use in OData queries
7
+ * @param {string} str - The string to escape
8
+ * @returns {string} - The escaped string
9
+ */
10
+ function escapeODataString(str) {
11
+ if (!str) return str;
12
+
13
+ // Replace single quotes with double single quotes (OData escaping)
14
+ // And remove any special characters that could cause OData syntax errors
15
+ str = str.replace(/'/g, "''");
16
+
17
+ // Escape other potentially problematic characters
18
+ str = str.replace(/[\(\)\{\}\[\]\:\;\,\/\?\&\=\+\*\%\$\#\@\!\^]/g, '');
19
+
20
+ console.error(`Escaped OData string: '${str}'`);
21
+ return str;
22
+ }
23
+
24
+ /**
25
+ * Builds an OData filter from filter conditions
26
+ * @param {Array<string>} conditions - Array of filter conditions
27
+ * @returns {string} - Combined OData filter expression
28
+ */
29
+ function buildODataFilter(conditions) {
30
+ if (!conditions || conditions.length === 0) {
31
+ return '';
32
+ }
33
+
34
+ return conditions.join(' and ');
35
+ }
36
+
37
+ module.exports = {
38
+ escapeODataString,
39
+ buildODataFilter
40
+ };