@littlebearapps/outlook-assistant 3.4.1 → 3.5.1
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 +8 -0
- package/README.md +42 -23
- package/auth/device-code.js +139 -0
- package/auth/index.js +16 -5
- package/auth/tools.js +187 -18
- package/config.js +7 -0
- package/email/export.js +2 -4
- package/email/index.js +43 -0
- package/email/mail-tips.js +211 -0
- package/email/send.js +44 -0
- package/llms-install.md +90 -0
- package/llms.txt +8 -7
- package/package.json +4 -3
- package/utils/graph-api.js +128 -5
package/utils/graph-api.js
CHANGED
|
@@ -12,6 +12,7 @@ const mockData = require('./mock-data');
|
|
|
12
12
|
* @param {string} path - API endpoint path
|
|
13
13
|
* @param {object} data - Data to send for POST/PUT requests
|
|
14
14
|
* @param {object} queryParams - Query parameters
|
|
15
|
+
* @param {object} extraHeaders - Additional headers (e.g. Prefer for immutable IDs)
|
|
15
16
|
* @returns {Promise<object>} - The API response
|
|
16
17
|
*/
|
|
17
18
|
async function callGraphAPI(
|
|
@@ -19,7 +20,8 @@ async function callGraphAPI(
|
|
|
19
20
|
method,
|
|
20
21
|
path,
|
|
21
22
|
data = null,
|
|
22
|
-
queryParams = {}
|
|
23
|
+
queryParams = {},
|
|
24
|
+
extraHeaders = {}
|
|
23
25
|
) {
|
|
24
26
|
// For test tokens, we'll simulate the API call
|
|
25
27
|
if (config.USE_TEST_MODE && accessToken.startsWith('test_access_token_')) {
|
|
@@ -75,12 +77,22 @@ async function callGraphAPI(
|
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
return new Promise((resolve, reject) => {
|
|
80
|
+
const headers = {
|
|
81
|
+
Authorization: `Bearer ${accessToken}`,
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Add immutable IDs header when enabled globally
|
|
86
|
+
if (config.USE_IMMUTABLE_IDS) {
|
|
87
|
+
headers.Prefer = 'IdType="ImmutableId"';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Merge any extra headers (caller overrides take precedence)
|
|
91
|
+
Object.assign(headers, extraHeaders);
|
|
92
|
+
|
|
78
93
|
const options = {
|
|
79
94
|
method: method,
|
|
80
|
-
headers
|
|
81
|
-
Authorization: `Bearer ${accessToken}`,
|
|
82
|
-
'Content-Type': 'application/json',
|
|
83
|
-
},
|
|
95
|
+
headers,
|
|
84
96
|
};
|
|
85
97
|
|
|
86
98
|
const req = https.request(finalUrl, options, (res) => {
|
|
@@ -202,6 +214,58 @@ async function callGraphAPIPaginated(
|
|
|
202
214
|
}
|
|
203
215
|
}
|
|
204
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Sends multiple Graph API requests in a single batch call ($batch).
|
|
219
|
+
* Supports up to 20 requests per batch (Graph API limit).
|
|
220
|
+
* @param {string} accessToken - The access token for authentication
|
|
221
|
+
* @param {Array<{id: string, method: string, url: string, body?: object, headers?: object}>} requests - Batch requests
|
|
222
|
+
* @returns {Promise<Array<{id: string, status: number, body: object}>>} - Array of responses
|
|
223
|
+
*/
|
|
224
|
+
async function callGraphAPIBatch(accessToken, requests) {
|
|
225
|
+
if (!Array.isArray(requests) || requests.length === 0) {
|
|
226
|
+
throw new Error('Batch requests must be a non-empty array');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (requests.length > 20) {
|
|
230
|
+
throw new Error('Batch requests cannot exceed 20 (Graph API limit)');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Test mode
|
|
234
|
+
if (config.USE_TEST_MODE && accessToken.startsWith('test_access_token_')) {
|
|
235
|
+
return requests.map((req) => ({
|
|
236
|
+
id: req.id,
|
|
237
|
+
status: 200,
|
|
238
|
+
body: mockData.simulateGraphAPIResponse(
|
|
239
|
+
req.method,
|
|
240
|
+
req.url,
|
|
241
|
+
req.body || null,
|
|
242
|
+
{}
|
|
243
|
+
),
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const batchPayload = {
|
|
248
|
+
requests: requests.map((req) => ({
|
|
249
|
+
id: req.id,
|
|
250
|
+
method: req.method,
|
|
251
|
+
url: req.url.startsWith('/') ? req.url : `/${req.url}`,
|
|
252
|
+
...(req.body && { body: req.body }),
|
|
253
|
+
...(req.headers && { headers: req.headers }),
|
|
254
|
+
})),
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const response = await callGraphAPI(
|
|
258
|
+
accessToken,
|
|
259
|
+
'POST',
|
|
260
|
+
'$batch',
|
|
261
|
+
batchPayload
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
return (response.responses || []).sort(
|
|
265
|
+
(a, b) => parseInt(a.id) - parseInt(b.id)
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
205
269
|
/**
|
|
206
270
|
* Calls Graph API to get raw MIME content (for email export)
|
|
207
271
|
* @param {string} accessToken - The access token for authentication
|
|
@@ -261,8 +325,67 @@ async function callGraphAPIRaw(accessToken, emailId) {
|
|
|
261
325
|
});
|
|
262
326
|
}
|
|
263
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Calls Graph API with automatic auth and 401 retry.
|
|
330
|
+
* Gets token via ensureAuthenticated(), and if a 401 occurs,
|
|
331
|
+
* refreshes the token and retries once.
|
|
332
|
+
* @param {string} method - HTTP method
|
|
333
|
+
* @param {string} path - API endpoint path
|
|
334
|
+
* @param {object} data - Request body
|
|
335
|
+
* @param {object} queryParams - Query parameters
|
|
336
|
+
* @param {object} extraHeaders - Additional headers
|
|
337
|
+
* @returns {Promise<object>} - API response
|
|
338
|
+
*/
|
|
339
|
+
async function callGraphAPIWithAuth(
|
|
340
|
+
method,
|
|
341
|
+
path,
|
|
342
|
+
data = null,
|
|
343
|
+
queryParams = {},
|
|
344
|
+
extraHeaders = {}
|
|
345
|
+
) {
|
|
346
|
+
// Lazy require to avoid circular dependency
|
|
347
|
+
const { ensureAuthenticated, tokenStorage } = require('../auth');
|
|
348
|
+
|
|
349
|
+
const accessToken = await ensureAuthenticated();
|
|
350
|
+
try {
|
|
351
|
+
return await callGraphAPI(
|
|
352
|
+
accessToken,
|
|
353
|
+
method,
|
|
354
|
+
path,
|
|
355
|
+
data,
|
|
356
|
+
queryParams,
|
|
357
|
+
extraHeaders
|
|
358
|
+
);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
if (error.message === 'UNAUTHORIZED' && tokenStorage) {
|
|
361
|
+
console.error('[GRAPH-API] 401 received, attempting token refresh...');
|
|
362
|
+
try {
|
|
363
|
+
const newToken = await tokenStorage.refreshAccessToken();
|
|
364
|
+
if (newToken) {
|
|
365
|
+
return await callGraphAPI(
|
|
366
|
+
newToken,
|
|
367
|
+
method,
|
|
368
|
+
path,
|
|
369
|
+
data,
|
|
370
|
+
queryParams,
|
|
371
|
+
extraHeaders
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
} catch (refreshError) {
|
|
375
|
+
console.error(
|
|
376
|
+
'[GRAPH-API] Token refresh failed:',
|
|
377
|
+
refreshError.message
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
264
385
|
module.exports = {
|
|
265
386
|
callGraphAPI,
|
|
266
387
|
callGraphAPIPaginated,
|
|
388
|
+
callGraphAPIBatch,
|
|
267
389
|
callGraphAPIRaw,
|
|
390
|
+
callGraphAPIWithAuth,
|
|
268
391
|
};
|