@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.
- package/.env.example +22 -0
- package/LICENSE +21 -0
- package/README.md +422 -0
- package/advanced/index.js +652 -0
- package/auth/index.js +32 -0
- package/auth/oauth-server.js +233 -0
- package/auth/token-manager.js +105 -0
- package/auth/token-storage.js +359 -0
- package/auth/tools.js +159 -0
- package/calendar/accept.js +72 -0
- package/calendar/cancel.js +72 -0
- package/calendar/create.js +115 -0
- package/calendar/decline.js +72 -0
- package/calendar/delete.js +67 -0
- package/calendar/index.js +130 -0
- package/calendar/list.js +108 -0
- package/categories/index.js +955 -0
- package/config.js +95 -0
- package/contacts/index.js +754 -0
- package/email/attachments.js +365 -0
- package/email/conversations.js +666 -0
- package/email/delta.js +210 -0
- package/email/export.js +572 -0
- package/email/folder-utils.js +192 -0
- package/email/headers.js +344 -0
- package/email/index.js +537 -0
- package/email/list.js +136 -0
- package/email/mark-as-read.js +114 -0
- package/email/mime.js +286 -0
- package/email/read.js +161 -0
- package/email/search.js +628 -0
- package/email/send.js +169 -0
- package/folder/create.js +137 -0
- package/folder/delete.js +108 -0
- package/folder/index.js +112 -0
- package/folder/list.js +289 -0
- package/folder/move.js +186 -0
- package/folder/stats.js +322 -0
- package/index.js +162 -0
- package/llms.txt +76 -0
- package/outlook-auth-server.js +384 -0
- package/package.json +97 -0
- package/rules/create.js +273 -0
- package/rules/index.js +276 -0
- package/rules/list.js +216 -0
- package/settings/index.js +678 -0
- package/utils/field-presets.js +311 -0
- package/utils/graph-api.js +268 -0
- package/utils/mock-data.js +154 -0
- package/utils/odata-helpers.js +33 -0
- package/utils/response-formatter.js +457 -0
- package/utils/safety.js +123 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field selection presets for Microsoft Graph API email queries
|
|
3
|
+
*
|
|
4
|
+
* Named presets for common use cases to optimize API calls
|
|
5
|
+
* and reduce response size/token usage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Field presets for different use cases
|
|
10
|
+
*/
|
|
11
|
+
const FIELD_PRESETS = {
|
|
12
|
+
/**
|
|
13
|
+
* Minimal fields for listing/scanning emails
|
|
14
|
+
* Use case: Quick folder scan, batch operations needing just IDs
|
|
15
|
+
*/
|
|
16
|
+
list: ['id', 'subject', 'from', 'receivedDateTime', 'isRead'],
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Minimal fields for reading a single email
|
|
20
|
+
* Use case: Quick read at minimal verbosity (includes bodyPreview + toRecipients)
|
|
21
|
+
*/
|
|
22
|
+
'read-minimal': [
|
|
23
|
+
'id',
|
|
24
|
+
'subject',
|
|
25
|
+
'from',
|
|
26
|
+
'toRecipients',
|
|
27
|
+
'receivedDateTime',
|
|
28
|
+
'bodyPreview',
|
|
29
|
+
'hasAttachments',
|
|
30
|
+
'importance',
|
|
31
|
+
'isRead',
|
|
32
|
+
],
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Standard fields for reading email content
|
|
36
|
+
* Use case: Viewing email details with body
|
|
37
|
+
*/
|
|
38
|
+
read: [
|
|
39
|
+
'id',
|
|
40
|
+
'subject',
|
|
41
|
+
'from',
|
|
42
|
+
'toRecipients',
|
|
43
|
+
'ccRecipients',
|
|
44
|
+
'receivedDateTime',
|
|
45
|
+
'body',
|
|
46
|
+
'bodyPreview',
|
|
47
|
+
'hasAttachments',
|
|
48
|
+
'importance',
|
|
49
|
+
'isRead',
|
|
50
|
+
],
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extended fields for legal/forensic use
|
|
54
|
+
* Use case: Evidence collection, compliance, legal review
|
|
55
|
+
*/
|
|
56
|
+
forensic: [
|
|
57
|
+
'id',
|
|
58
|
+
'subject',
|
|
59
|
+
'from',
|
|
60
|
+
'toRecipients',
|
|
61
|
+
'ccRecipients',
|
|
62
|
+
'bccRecipients',
|
|
63
|
+
'receivedDateTime',
|
|
64
|
+
'sentDateTime',
|
|
65
|
+
'body',
|
|
66
|
+
'bodyPreview',
|
|
67
|
+
'hasAttachments',
|
|
68
|
+
'importance',
|
|
69
|
+
'isRead',
|
|
70
|
+
'internetMessageHeaders',
|
|
71
|
+
'internetMessageId',
|
|
72
|
+
'conversationId',
|
|
73
|
+
'conversationIndex',
|
|
74
|
+
'parentFolderId',
|
|
75
|
+
],
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Full fields for export operations
|
|
79
|
+
* Use case: Complete email backup, migration, archival
|
|
80
|
+
*/
|
|
81
|
+
export: [
|
|
82
|
+
'id',
|
|
83
|
+
'subject',
|
|
84
|
+
'from',
|
|
85
|
+
'toRecipients',
|
|
86
|
+
'ccRecipients',
|
|
87
|
+
'bccRecipients',
|
|
88
|
+
'replyTo',
|
|
89
|
+
'receivedDateTime',
|
|
90
|
+
'sentDateTime',
|
|
91
|
+
'createdDateTime',
|
|
92
|
+
'lastModifiedDateTime',
|
|
93
|
+
'body',
|
|
94
|
+
'bodyPreview',
|
|
95
|
+
'hasAttachments',
|
|
96
|
+
'importance',
|
|
97
|
+
'isRead',
|
|
98
|
+
'isDraft',
|
|
99
|
+
'internetMessageHeaders',
|
|
100
|
+
'internetMessageId',
|
|
101
|
+
'conversationId',
|
|
102
|
+
'conversationIndex',
|
|
103
|
+
'parentFolderId',
|
|
104
|
+
'categories',
|
|
105
|
+
'flag',
|
|
106
|
+
'webLink',
|
|
107
|
+
'changeKey',
|
|
108
|
+
],
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Search result fields (optimized for relevance display)
|
|
112
|
+
* Use case: Search results with context
|
|
113
|
+
*/
|
|
114
|
+
search: [
|
|
115
|
+
'id',
|
|
116
|
+
'subject',
|
|
117
|
+
'from',
|
|
118
|
+
'toRecipients',
|
|
119
|
+
'receivedDateTime',
|
|
120
|
+
'bodyPreview',
|
|
121
|
+
'hasAttachments',
|
|
122
|
+
'importance',
|
|
123
|
+
'isRead',
|
|
124
|
+
'parentFolderId',
|
|
125
|
+
],
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Delta query fields (for incremental sync)
|
|
129
|
+
* Use case: Tracking changes in folder
|
|
130
|
+
*/
|
|
131
|
+
delta: [
|
|
132
|
+
'id',
|
|
133
|
+
'subject',
|
|
134
|
+
'from',
|
|
135
|
+
'receivedDateTime',
|
|
136
|
+
'isRead',
|
|
137
|
+
'parentFolderId',
|
|
138
|
+
'changeKey',
|
|
139
|
+
],
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Conversation fields (for thread grouping)
|
|
143
|
+
* Use case: Viewing email threads
|
|
144
|
+
*/
|
|
145
|
+
conversation: [
|
|
146
|
+
'id',
|
|
147
|
+
'subject',
|
|
148
|
+
'from',
|
|
149
|
+
'toRecipients',
|
|
150
|
+
'receivedDateTime',
|
|
151
|
+
'bodyPreview',
|
|
152
|
+
'conversationId',
|
|
153
|
+
'conversationIndex',
|
|
154
|
+
'isRead',
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Extended email fields (comprehensive list)
|
|
160
|
+
* Used as reference for export preset
|
|
161
|
+
*/
|
|
162
|
+
const EXTENDED_EMAIL_FIELDS = [
|
|
163
|
+
'id',
|
|
164
|
+
'subject',
|
|
165
|
+
'from',
|
|
166
|
+
'sender',
|
|
167
|
+
'toRecipients',
|
|
168
|
+
'ccRecipients',
|
|
169
|
+
'bccRecipients',
|
|
170
|
+
'replyTo',
|
|
171
|
+
'receivedDateTime',
|
|
172
|
+
'sentDateTime',
|
|
173
|
+
'createdDateTime',
|
|
174
|
+
'lastModifiedDateTime',
|
|
175
|
+
'body',
|
|
176
|
+
'bodyPreview',
|
|
177
|
+
'hasAttachments',
|
|
178
|
+
'importance',
|
|
179
|
+
'isRead',
|
|
180
|
+
'isDraft',
|
|
181
|
+
'isDeliveryReceiptRequested',
|
|
182
|
+
'isReadReceiptRequested',
|
|
183
|
+
'internetMessageHeaders',
|
|
184
|
+
'internetMessageId',
|
|
185
|
+
'conversationId',
|
|
186
|
+
'conversationIndex',
|
|
187
|
+
'parentFolderId',
|
|
188
|
+
'categories',
|
|
189
|
+
'flag',
|
|
190
|
+
'webLink',
|
|
191
|
+
'changeKey',
|
|
192
|
+
'inferenceClassification',
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Folder fields for folder operations
|
|
197
|
+
*/
|
|
198
|
+
const FOLDER_FIELDS = {
|
|
199
|
+
/**
|
|
200
|
+
* Basic folder info
|
|
201
|
+
*/
|
|
202
|
+
basic: ['id', 'displayName', 'parentFolderId'],
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Folder with item counts
|
|
206
|
+
*/
|
|
207
|
+
withCounts: [
|
|
208
|
+
'id',
|
|
209
|
+
'displayName',
|
|
210
|
+
'parentFolderId',
|
|
211
|
+
'totalItemCount',
|
|
212
|
+
'unreadItemCount',
|
|
213
|
+
],
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Full folder details
|
|
217
|
+
* Note: sizeInBytes is NOT available on mailFolder resource type
|
|
218
|
+
*/
|
|
219
|
+
full: [
|
|
220
|
+
'id',
|
|
221
|
+
'displayName',
|
|
222
|
+
'parentFolderId',
|
|
223
|
+
'childFolderCount',
|
|
224
|
+
'totalItemCount',
|
|
225
|
+
'unreadItemCount',
|
|
226
|
+
'isHidden',
|
|
227
|
+
],
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Gets field selection string for Graph API $select parameter
|
|
232
|
+
* @param {string} preset - Preset name (list, read, forensic, export, search, delta, conversation)
|
|
233
|
+
* @returns {string} - Comma-separated field list
|
|
234
|
+
*/
|
|
235
|
+
function getEmailFields(preset = 'list') {
|
|
236
|
+
const fields = FIELD_PRESETS[preset];
|
|
237
|
+
if (!fields) {
|
|
238
|
+
console.error(`Unknown preset: ${preset}, falling back to 'list'`);
|
|
239
|
+
return FIELD_PRESETS.list.join(',');
|
|
240
|
+
}
|
|
241
|
+
return fields.join(',');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Gets folder field selection string
|
|
246
|
+
* @param {string} preset - Preset name (basic, withCounts, full)
|
|
247
|
+
* @returns {string} - Comma-separated field list
|
|
248
|
+
*/
|
|
249
|
+
function getFolderFields(preset = 'basic') {
|
|
250
|
+
const fields = FOLDER_FIELDS[preset];
|
|
251
|
+
if (!fields) {
|
|
252
|
+
console.error(`Unknown folder preset: ${preset}, falling back to 'basic'`);
|
|
253
|
+
return FOLDER_FIELDS.basic.join(',');
|
|
254
|
+
}
|
|
255
|
+
return fields.join(',');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Builds custom field selection from array
|
|
260
|
+
* @param {Array<string>} fields - Array of field names
|
|
261
|
+
* @returns {string} - Comma-separated field list
|
|
262
|
+
*/
|
|
263
|
+
function buildFieldSelection(fields) {
|
|
264
|
+
if (!fields || !Array.isArray(fields) || fields.length === 0) {
|
|
265
|
+
return getEmailFields('list');
|
|
266
|
+
}
|
|
267
|
+
return fields.join(',');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Merges preset with additional fields
|
|
272
|
+
* @param {string} preset - Base preset name
|
|
273
|
+
* @param {Array<string>} additionalFields - Extra fields to include
|
|
274
|
+
* @returns {string} - Combined field selection
|
|
275
|
+
*/
|
|
276
|
+
function mergeFields(preset, additionalFields = []) {
|
|
277
|
+
const baseFields = FIELD_PRESETS[preset] || FIELD_PRESETS.list;
|
|
278
|
+
const merged = [...new Set([...baseFields, ...additionalFields])];
|
|
279
|
+
return merged.join(',');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Validates that requested fields are valid Graph API fields
|
|
284
|
+
* @param {Array<string>} fields - Fields to validate
|
|
285
|
+
* @returns {object} - { valid: Array, invalid: Array }
|
|
286
|
+
*/
|
|
287
|
+
function validateFields(fields) {
|
|
288
|
+
const validFields = new Set(EXTENDED_EMAIL_FIELDS);
|
|
289
|
+
const result = { valid: [], invalid: [] };
|
|
290
|
+
|
|
291
|
+
fields.forEach((field) => {
|
|
292
|
+
if (validFields.has(field)) {
|
|
293
|
+
result.valid.push(field);
|
|
294
|
+
} else {
|
|
295
|
+
result.invalid.push(field);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
module.exports = {
|
|
303
|
+
FIELD_PRESETS,
|
|
304
|
+
EXTENDED_EMAIL_FIELDS,
|
|
305
|
+
FOLDER_FIELDS,
|
|
306
|
+
getEmailFields,
|
|
307
|
+
getFolderFields,
|
|
308
|
+
buildFieldSelection,
|
|
309
|
+
mergeFields,
|
|
310
|
+
validateFields,
|
|
311
|
+
};
|
|
@@ -0,0 +1,268 @@
|
|
|
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(
|
|
18
|
+
accessToken,
|
|
19
|
+
method,
|
|
20
|
+
path,
|
|
21
|
+
data = null,
|
|
22
|
+
queryParams = {}
|
|
23
|
+
) {
|
|
24
|
+
// For test tokens, we'll simulate the API call
|
|
25
|
+
if (config.USE_TEST_MODE && accessToken.startsWith('test_access_token_')) {
|
|
26
|
+
return mockData.simulateGraphAPIResponse(method, path, data, queryParams);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Check if path already contains the full URL (from nextLink)
|
|
31
|
+
let finalUrl;
|
|
32
|
+
if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
33
|
+
// Path is already a full URL (from pagination nextLink)
|
|
34
|
+
finalUrl = path;
|
|
35
|
+
} else {
|
|
36
|
+
// Build URL from path and queryParams
|
|
37
|
+
// Encode path segments properly
|
|
38
|
+
const encodedPath = path
|
|
39
|
+
.split('/')
|
|
40
|
+
.map((segment) => encodeURIComponent(segment))
|
|
41
|
+
.join('/');
|
|
42
|
+
|
|
43
|
+
// Build query string from parameters with special handling for OData filters
|
|
44
|
+
let queryString = '';
|
|
45
|
+
if (Object.keys(queryParams).length > 0) {
|
|
46
|
+
// Handle $filter parameter specially to ensure proper URI encoding
|
|
47
|
+
const filter = queryParams.$filter;
|
|
48
|
+
if (filter) {
|
|
49
|
+
delete queryParams.$filter; // Remove from regular params
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Build query string with proper encoding for regular params
|
|
53
|
+
const params = new URLSearchParams();
|
|
54
|
+
for (const [key, value] of Object.entries(queryParams)) {
|
|
55
|
+
params.append(key, value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
queryString = params.toString();
|
|
59
|
+
|
|
60
|
+
// Add filter parameter separately with proper encoding
|
|
61
|
+
if (filter) {
|
|
62
|
+
if (queryString) {
|
|
63
|
+
queryString += `&$filter=${encodeURIComponent(filter)}`;
|
|
64
|
+
} else {
|
|
65
|
+
queryString = `$filter=${encodeURIComponent(filter)}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (queryString) {
|
|
70
|
+
queryString = '?' + queryString;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
finalUrl = `${config.GRAPH_API_ENDPOINT}${encodedPath}${queryString}`;
|
|
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
|
+
// Truncate response to avoid leaking sensitive data in error messages
|
|
107
|
+
const safeResponse = responseData.substring(0, 200);
|
|
108
|
+
reject(
|
|
109
|
+
new Error(
|
|
110
|
+
`API call failed with status ${res.statusCode}: ${safeResponse}`
|
|
111
|
+
)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
req.on('error', (error) => {
|
|
118
|
+
reject(new Error(`Network error during API call: ${error.message}`));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
data &&
|
|
123
|
+
(method === 'POST' || method === 'PATCH' || method === 'PUT')
|
|
124
|
+
) {
|
|
125
|
+
req.write(JSON.stringify(data));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
req.end();
|
|
129
|
+
});
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('Error calling Graph API:', error);
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Calls Graph API with pagination support to retrieve all results up to maxCount
|
|
138
|
+
* @param {string} accessToken - The access token for authentication
|
|
139
|
+
* @param {string} method - HTTP method (GET only for pagination)
|
|
140
|
+
* @param {string} path - API endpoint path
|
|
141
|
+
* @param {object} queryParams - Initial query parameters
|
|
142
|
+
* @param {number} maxCount - Maximum number of items to retrieve (0 = all)
|
|
143
|
+
* @returns {Promise<object>} - Combined API response with all items
|
|
144
|
+
*/
|
|
145
|
+
async function callGraphAPIPaginated(
|
|
146
|
+
accessToken,
|
|
147
|
+
method,
|
|
148
|
+
path,
|
|
149
|
+
queryParams = {},
|
|
150
|
+
maxCount = 0
|
|
151
|
+
) {
|
|
152
|
+
if (method !== 'GET') {
|
|
153
|
+
throw new Error('Pagination only supports GET requests');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const allItems = [];
|
|
157
|
+
let nextLink;
|
|
158
|
+
let currentUrl = path;
|
|
159
|
+
let currentParams = { ...queryParams };
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
do {
|
|
163
|
+
// Make API call
|
|
164
|
+
const response = await callGraphAPI(
|
|
165
|
+
accessToken,
|
|
166
|
+
method,
|
|
167
|
+
currentUrl,
|
|
168
|
+
null,
|
|
169
|
+
currentParams
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Add items from this page
|
|
173
|
+
if (response.value && Array.isArray(response.value)) {
|
|
174
|
+
allItems.push(...response.value);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check if we've reached the desired count
|
|
178
|
+
if (maxCount > 0 && allItems.length >= maxCount) {
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Get next page URL
|
|
183
|
+
nextLink = response['@odata.nextLink'];
|
|
184
|
+
|
|
185
|
+
if (nextLink) {
|
|
186
|
+
// Pass the full nextLink URL directly to callGraphAPI
|
|
187
|
+
currentUrl = nextLink;
|
|
188
|
+
currentParams = {}; // nextLink already contains all params
|
|
189
|
+
}
|
|
190
|
+
} while (nextLink);
|
|
191
|
+
|
|
192
|
+
// Trim to exact count if needed
|
|
193
|
+
const finalItems = maxCount > 0 ? allItems.slice(0, maxCount) : allItems;
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
value: finalItems,
|
|
197
|
+
'@odata.count': finalItems.length,
|
|
198
|
+
};
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error('Error during pagination:', error);
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Calls Graph API to get raw MIME content (for email export)
|
|
207
|
+
* @param {string} accessToken - The access token for authentication
|
|
208
|
+
* @param {string} emailId - The email ID to export
|
|
209
|
+
* @returns {Promise<string>} - Raw MIME content as string
|
|
210
|
+
*/
|
|
211
|
+
async function callGraphAPIRaw(accessToken, emailId) {
|
|
212
|
+
// Test mode: return mock MIME content
|
|
213
|
+
if (config.USE_TEST_MODE && accessToken.startsWith('test_access_token_')) {
|
|
214
|
+
return mockData.getMockMimeContent
|
|
215
|
+
? mockData.getMockMimeContent(emailId)
|
|
216
|
+
: `MIME-Version: 1.0\nContent-Type: text/plain\n\nTest email content for ${emailId}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
const path = `me/messages/${encodeURIComponent(emailId)}/$value`;
|
|
221
|
+
const finalUrl = `${config.GRAPH_API_ENDPOINT}${path}`;
|
|
222
|
+
|
|
223
|
+
const options = {
|
|
224
|
+
method: 'GET',
|
|
225
|
+
headers: {
|
|
226
|
+
Authorization: `Bearer ${accessToken}`,
|
|
227
|
+
Accept: 'message/rfc822', // Request MIME format
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const req = https.request(finalUrl, options, (res) => {
|
|
232
|
+
let responseData = '';
|
|
233
|
+
|
|
234
|
+
// Collect data as UTF-8 string
|
|
235
|
+
res.setEncoding('utf8');
|
|
236
|
+
|
|
237
|
+
res.on('data', (chunk) => {
|
|
238
|
+
responseData += chunk;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
res.on('end', () => {
|
|
242
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
243
|
+
resolve(responseData);
|
|
244
|
+
} else if (res.statusCode === 401) {
|
|
245
|
+
reject(new Error('UNAUTHORIZED'));
|
|
246
|
+
} else {
|
|
247
|
+
reject(
|
|
248
|
+
new Error(
|
|
249
|
+
`MIME export failed with status ${res.statusCode}: ${responseData.substring(0, 200)}`
|
|
250
|
+
)
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
req.on('error', (error) => {
|
|
257
|
+
reject(new Error(`Network error during MIME export: ${error.message}`));
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
req.end();
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = {
|
|
265
|
+
callGraphAPI,
|
|
266
|
+
callGraphAPIPaginated,
|
|
267
|
+
callGraphAPIRaw,
|
|
268
|
+
};
|