ms365-mcp-server 1.1.15 → 1.1.17
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/dist/index.js +1123 -10
- package/dist/utils/batch-performance-monitor.js +106 -0
- package/dist/utils/batch-test-scenarios.js +277 -0
- package/dist/utils/context-aware-search.js +499 -0
- package/dist/utils/cross-reference-detector.js +352 -0
- package/dist/utils/document-workflow.js +433 -0
- package/dist/utils/enhanced-fuzzy-search.js +514 -0
- package/dist/utils/error-handler.js +337 -0
- package/dist/utils/intelligence-engine.js +71 -0
- package/dist/utils/intelligent-cache.js +379 -0
- package/dist/utils/large-mailbox-search.js +599 -0
- package/dist/utils/ms365-operations.js +799 -219
- package/dist/utils/performance-monitor.js +395 -0
- package/dist/utils/proactive-intelligence.js +390 -0
- package/dist/utils/rate-limiter.js +284 -0
- package/dist/utils/search-batch-pipeline.js +222 -0
- package/dist/utils/thread-reconstruction.js +700 -0
- package/package.json +1 -1
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { logger } from './api.js';
|
|
2
|
+
export var EmailCategory;
|
|
3
|
+
(function (EmailCategory) {
|
|
4
|
+
EmailCategory["GOVERNMENT"] = "government";
|
|
5
|
+
EmailCategory["TAX"] = "tax";
|
|
6
|
+
EmailCategory["LEGAL"] = "legal";
|
|
7
|
+
EmailCategory["FINANCIAL"] = "financial";
|
|
8
|
+
EmailCategory["HEALTHCARE"] = "healthcare";
|
|
9
|
+
EmailCategory["INSURANCE"] = "insurance";
|
|
10
|
+
EmailCategory["DEADLINE"] = "deadline";
|
|
11
|
+
EmailCategory["INVOICE"] = "invoice";
|
|
12
|
+
EmailCategory["CONTRACT"] = "contract";
|
|
13
|
+
EmailCategory["SECURITY"] = "security";
|
|
14
|
+
EmailCategory["COMPLIANCE"] = "compliance";
|
|
15
|
+
EmailCategory["NOTIFICATION"] = "notification";
|
|
16
|
+
EmailCategory["SPAM"] = "spam";
|
|
17
|
+
EmailCategory["PERSONAL"] = "personal";
|
|
18
|
+
EmailCategory["BUSINESS"] = "business";
|
|
19
|
+
EmailCategory["URGENT"] = "urgent";
|
|
20
|
+
EmailCategory["AUTOMATED"] = "automated";
|
|
21
|
+
})(EmailCategory || (EmailCategory = {}));
|
|
22
|
+
export class ProactiveIntelligence {
|
|
23
|
+
constructor(ms365Operations) {
|
|
24
|
+
this.ms365Operations = ms365Operations;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Classify a single email
|
|
28
|
+
*/
|
|
29
|
+
classifyEmail(email) {
|
|
30
|
+
const matchedRules = [];
|
|
31
|
+
const categories = [];
|
|
32
|
+
const reasons = [];
|
|
33
|
+
let maxPriority = 'low';
|
|
34
|
+
let totalConfidence = 0;
|
|
35
|
+
for (const rule of ProactiveIntelligence.CLASSIFICATION_RULES) {
|
|
36
|
+
const match = this.evaluateRule(email, rule);
|
|
37
|
+
if (match.isMatch) {
|
|
38
|
+
matchedRules.push(rule.id);
|
|
39
|
+
categories.push(rule.category);
|
|
40
|
+
reasons.push(match.reason);
|
|
41
|
+
totalConfidence += match.confidence;
|
|
42
|
+
// Update priority
|
|
43
|
+
if (this.getPriorityScore(rule.priority) > this.getPriorityScore(maxPriority)) {
|
|
44
|
+
maxPriority = rule.priority;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const averageConfidence = matchedRules.length > 0 ? totalConfidence / matchedRules.length : 0;
|
|
49
|
+
const actions = this.generateActions(categories, maxPriority);
|
|
50
|
+
return {
|
|
51
|
+
email,
|
|
52
|
+
categories,
|
|
53
|
+
priority: maxPriority,
|
|
54
|
+
confidence: averageConfidence,
|
|
55
|
+
matchedRules,
|
|
56
|
+
reasons,
|
|
57
|
+
actions
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Evaluate a classification rule against an email
|
|
62
|
+
*/
|
|
63
|
+
evaluateRule(email, rule) {
|
|
64
|
+
let matches = 0;
|
|
65
|
+
let totalChecks = 0;
|
|
66
|
+
const matchDetails = [];
|
|
67
|
+
const emailText = {
|
|
68
|
+
senderDomain: email.from.address.split('@')[1]?.toLowerCase() || '',
|
|
69
|
+
senderName: email.from.name.toLowerCase(),
|
|
70
|
+
subject: email.subject.toLowerCase(),
|
|
71
|
+
body: email.bodyPreview.toLowerCase(),
|
|
72
|
+
combined: `${email.from.name} ${email.subject} ${email.bodyPreview}`.toLowerCase()
|
|
73
|
+
};
|
|
74
|
+
// Check sender domains
|
|
75
|
+
if (rule.patterns.senderDomains) {
|
|
76
|
+
totalChecks++;
|
|
77
|
+
for (const domain of rule.patterns.senderDomains) {
|
|
78
|
+
if (emailText.senderDomain.includes(domain.toLowerCase())) {
|
|
79
|
+
matches++;
|
|
80
|
+
matchDetails.push(`sender domain: ${domain}`);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Check sender names
|
|
86
|
+
if (rule.patterns.senderNames) {
|
|
87
|
+
totalChecks++;
|
|
88
|
+
for (const name of rule.patterns.senderNames) {
|
|
89
|
+
if (emailText.senderName.includes(name.toLowerCase())) {
|
|
90
|
+
matches++;
|
|
91
|
+
matchDetails.push(`sender name: ${name}`);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Check subject keywords
|
|
97
|
+
if (rule.patterns.subjectKeywords) {
|
|
98
|
+
totalChecks++;
|
|
99
|
+
for (const keyword of rule.patterns.subjectKeywords) {
|
|
100
|
+
if (emailText.subject.includes(keyword.toLowerCase())) {
|
|
101
|
+
matches++;
|
|
102
|
+
matchDetails.push(`subject keyword: ${keyword}`);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Check body keywords
|
|
108
|
+
if (rule.patterns.bodyKeywords) {
|
|
109
|
+
totalChecks++;
|
|
110
|
+
for (const keyword of rule.patterns.bodyKeywords) {
|
|
111
|
+
if (emailText.body.includes(keyword.toLowerCase())) {
|
|
112
|
+
matches++;
|
|
113
|
+
matchDetails.push(`body keyword: ${keyword}`);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Check combined keywords
|
|
119
|
+
if (rule.patterns.combinedKeywords) {
|
|
120
|
+
totalChecks++;
|
|
121
|
+
for (const keyword of rule.patterns.combinedKeywords) {
|
|
122
|
+
if (emailText.combined.includes(keyword.toLowerCase())) {
|
|
123
|
+
matches++;
|
|
124
|
+
matchDetails.push(`combined keyword: ${keyword}`);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Determine if rule matches
|
|
130
|
+
let isMatch = false;
|
|
131
|
+
const confidence = totalChecks > 0 ? matches / totalChecks : 0;
|
|
132
|
+
if (rule.conditions.matchType === 'any') {
|
|
133
|
+
isMatch = matches > 0;
|
|
134
|
+
}
|
|
135
|
+
else if (rule.conditions.matchType === 'all') {
|
|
136
|
+
isMatch = matches === totalChecks;
|
|
137
|
+
}
|
|
138
|
+
if (rule.conditions.minimumMatches) {
|
|
139
|
+
isMatch = matches >= rule.conditions.minimumMatches;
|
|
140
|
+
}
|
|
141
|
+
if (rule.conditions.confidenceThreshold) {
|
|
142
|
+
isMatch = isMatch && confidence >= rule.conditions.confidenceThreshold;
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
isMatch,
|
|
146
|
+
confidence,
|
|
147
|
+
reason: `${rule.name}: ${matchDetails.join(', ')}`
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get priority score for comparison
|
|
152
|
+
*/
|
|
153
|
+
getPriorityScore(priority) {
|
|
154
|
+
switch (priority) {
|
|
155
|
+
case 'critical': return 4;
|
|
156
|
+
case 'high': return 3;
|
|
157
|
+
case 'medium': return 2;
|
|
158
|
+
case 'low': return 1;
|
|
159
|
+
default: return 0;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Generate recommended actions based on classification
|
|
164
|
+
*/
|
|
165
|
+
generateActions(categories, priority) {
|
|
166
|
+
const actions = [];
|
|
167
|
+
if (priority === 'critical') {
|
|
168
|
+
actions.push('Mark as high priority');
|
|
169
|
+
actions.push('Add to urgent folder');
|
|
170
|
+
}
|
|
171
|
+
if (categories.includes(EmailCategory.GOVERNMENT)) {
|
|
172
|
+
actions.push('File in government folder');
|
|
173
|
+
actions.push('Set reminder for response');
|
|
174
|
+
}
|
|
175
|
+
if (categories.includes(EmailCategory.TAX)) {
|
|
176
|
+
actions.push('File in tax documents');
|
|
177
|
+
actions.push('Check for deadline');
|
|
178
|
+
}
|
|
179
|
+
if (categories.includes(EmailCategory.DEADLINE)) {
|
|
180
|
+
actions.push('Set calendar reminder');
|
|
181
|
+
actions.push('Flag for follow-up');
|
|
182
|
+
}
|
|
183
|
+
if (categories.includes(EmailCategory.LEGAL)) {
|
|
184
|
+
actions.push('File in legal folder');
|
|
185
|
+
actions.push('Consider legal review');
|
|
186
|
+
}
|
|
187
|
+
if (categories.includes(EmailCategory.FINANCIAL)) {
|
|
188
|
+
actions.push('File in financial folder');
|
|
189
|
+
actions.push('Verify sender authenticity');
|
|
190
|
+
}
|
|
191
|
+
return actions;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Batch classify multiple emails
|
|
195
|
+
*/
|
|
196
|
+
async batchClassifyEmails(emails) {
|
|
197
|
+
const results = new Map();
|
|
198
|
+
logger.log(`🎯 Classifying ${emails.length} emails`);
|
|
199
|
+
for (const email of emails) {
|
|
200
|
+
try {
|
|
201
|
+
const classification = this.classifyEmail(email);
|
|
202
|
+
results.set(email.id, classification);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
logger.error(`Error classifying email ${email.id}:`, error);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const criticalEmails = Array.from(results.values()).filter(r => r.priority === 'critical').length;
|
|
209
|
+
const highPriorityEmails = Array.from(results.values()).filter(r => r.priority === 'high').length;
|
|
210
|
+
logger.log(`🎯 Classification complete: ${criticalEmails} critical, ${highPriorityEmails} high priority`);
|
|
211
|
+
return results;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Get emails by category
|
|
215
|
+
*/
|
|
216
|
+
getEmailsByCategory(classifications, category) {
|
|
217
|
+
return Array.from(classifications.values())
|
|
218
|
+
.filter(result => result.categories.includes(category))
|
|
219
|
+
.sort((a, b) => this.getPriorityScore(b.priority) - this.getPriorityScore(a.priority));
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get high priority emails
|
|
223
|
+
*/
|
|
224
|
+
getHighPriorityEmails(classifications) {
|
|
225
|
+
return Array.from(classifications.values())
|
|
226
|
+
.filter(result => result.priority === 'critical' || result.priority === 'high')
|
|
227
|
+
.sort((a, b) => this.getPriorityScore(b.priority) - this.getPriorityScore(a.priority));
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Generate classification summary
|
|
231
|
+
*/
|
|
232
|
+
generateClassificationSummary(classifications) {
|
|
233
|
+
const results = Array.from(classifications.values());
|
|
234
|
+
const byPriority = {
|
|
235
|
+
critical: results.filter(r => r.priority === 'critical').length,
|
|
236
|
+
high: results.filter(r => r.priority === 'high').length,
|
|
237
|
+
medium: results.filter(r => r.priority === 'medium').length,
|
|
238
|
+
low: results.filter(r => r.priority === 'low').length
|
|
239
|
+
};
|
|
240
|
+
const categoryCount = new Map();
|
|
241
|
+
const reasonCount = new Map();
|
|
242
|
+
const actionCount = new Map();
|
|
243
|
+
for (const result of results) {
|
|
244
|
+
// Count categories
|
|
245
|
+
for (const category of result.categories) {
|
|
246
|
+
categoryCount.set(category, (categoryCount.get(category) || 0) + 1);
|
|
247
|
+
}
|
|
248
|
+
// Count reasons
|
|
249
|
+
for (const reason of result.reasons) {
|
|
250
|
+
reasonCount.set(reason, (reasonCount.get(reason) || 0) + 1);
|
|
251
|
+
}
|
|
252
|
+
// Count actions
|
|
253
|
+
for (const action of result.actions) {
|
|
254
|
+
actionCount.set(action, (actionCount.get(action) || 0) + 1);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const byCategory = Object.fromEntries(categoryCount);
|
|
258
|
+
const topReasons = Array.from(reasonCount.entries())
|
|
259
|
+
.sort((a, b) => b[1] - a[1])
|
|
260
|
+
.slice(0, 10)
|
|
261
|
+
.map(([reason]) => reason);
|
|
262
|
+
const recommendedActions = Array.from(actionCount.entries())
|
|
263
|
+
.sort((a, b) => b[1] - a[1])
|
|
264
|
+
.slice(0, 10)
|
|
265
|
+
.map(([action]) => action);
|
|
266
|
+
return {
|
|
267
|
+
totalEmails: results.length,
|
|
268
|
+
byPriority,
|
|
269
|
+
byCategory,
|
|
270
|
+
topReasons,
|
|
271
|
+
recommendedActions
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
ProactiveIntelligence.CLASSIFICATION_RULES = [
|
|
276
|
+
// Government emails
|
|
277
|
+
{
|
|
278
|
+
id: 'gov_federal',
|
|
279
|
+
name: 'Federal Government',
|
|
280
|
+
category: EmailCategory.GOVERNMENT,
|
|
281
|
+
priority: 'critical',
|
|
282
|
+
patterns: {
|
|
283
|
+
senderDomains: ['.gov', '.mil', 'irs.gov', 'ssa.gov', 'treasury.gov'],
|
|
284
|
+
senderNames: ['Internal Revenue Service', 'Social Security', 'Treasury', 'IRS'],
|
|
285
|
+
combinedKeywords: ['federal', 'government', 'official', 'notice']
|
|
286
|
+
},
|
|
287
|
+
conditions: { matchType: 'any', confidenceThreshold: 0.8 }
|
|
288
|
+
},
|
|
289
|
+
// Tax notices
|
|
290
|
+
{
|
|
291
|
+
id: 'tax_notice',
|
|
292
|
+
name: 'Tax Notice',
|
|
293
|
+
category: EmailCategory.TAX,
|
|
294
|
+
priority: 'critical',
|
|
295
|
+
patterns: {
|
|
296
|
+
subjectKeywords: ['tax', 'irs', 'notice', 'refund', 'audit', 'filing'],
|
|
297
|
+
bodyKeywords: ['tax return', 'refund', 'audit', 'tax due', 'payment'],
|
|
298
|
+
senderDomains: ['irs.gov', 'treasury.gov']
|
|
299
|
+
},
|
|
300
|
+
conditions: { matchType: 'any', minimumMatches: 2 }
|
|
301
|
+
},
|
|
302
|
+
// Legal documents
|
|
303
|
+
{
|
|
304
|
+
id: 'legal_doc',
|
|
305
|
+
name: 'Legal Document',
|
|
306
|
+
category: EmailCategory.LEGAL,
|
|
307
|
+
priority: 'high',
|
|
308
|
+
patterns: {
|
|
309
|
+
subjectKeywords: ['legal', 'court', 'lawsuit', 'summons', 'subpoena'],
|
|
310
|
+
bodyKeywords: ['attorney', 'lawyer', 'court', 'legal action', 'litigation'],
|
|
311
|
+
senderNames: ['Attorney', 'Legal', 'Court', 'Law Firm']
|
|
312
|
+
},
|
|
313
|
+
conditions: { matchType: 'any', minimumMatches: 1 }
|
|
314
|
+
},
|
|
315
|
+
// Financial institutions
|
|
316
|
+
{
|
|
317
|
+
id: 'financial_inst',
|
|
318
|
+
name: 'Financial Institution',
|
|
319
|
+
category: EmailCategory.FINANCIAL,
|
|
320
|
+
priority: 'high',
|
|
321
|
+
patterns: {
|
|
322
|
+
senderDomains: ['bank', 'credit', 'financial', 'investment'],
|
|
323
|
+
subjectKeywords: ['account', 'statement', 'balance', 'transaction'],
|
|
324
|
+
bodyKeywords: ['account number', 'balance', 'transaction', 'payment']
|
|
325
|
+
},
|
|
326
|
+
conditions: { matchType: 'any', minimumMatches: 1 }
|
|
327
|
+
},
|
|
328
|
+
// Healthcare
|
|
329
|
+
{
|
|
330
|
+
id: 'healthcare',
|
|
331
|
+
name: 'Healthcare',
|
|
332
|
+
category: EmailCategory.HEALTHCARE,
|
|
333
|
+
priority: 'high',
|
|
334
|
+
patterns: {
|
|
335
|
+
senderNames: ['Hospital', 'Medical', 'Doctor', 'Clinic', 'Health'],
|
|
336
|
+
subjectKeywords: ['appointment', 'medical', 'health', 'prescription'],
|
|
337
|
+
bodyKeywords: ['patient', 'medical', 'health', 'appointment', 'prescription']
|
|
338
|
+
},
|
|
339
|
+
conditions: { matchType: 'any', minimumMatches: 1 }
|
|
340
|
+
},
|
|
341
|
+
// Insurance
|
|
342
|
+
{
|
|
343
|
+
id: 'insurance',
|
|
344
|
+
name: 'Insurance',
|
|
345
|
+
category: EmailCategory.INSURANCE,
|
|
346
|
+
priority: 'high',
|
|
347
|
+
patterns: {
|
|
348
|
+
senderNames: ['Insurance', 'Policy', 'Claims'],
|
|
349
|
+
subjectKeywords: ['policy', 'claim', 'coverage', 'premium'],
|
|
350
|
+
bodyKeywords: ['policy number', 'claim', 'coverage', 'premium', 'deductible']
|
|
351
|
+
},
|
|
352
|
+
conditions: { matchType: 'any', minimumMatches: 1 }
|
|
353
|
+
},
|
|
354
|
+
// Deadline-sensitive
|
|
355
|
+
{
|
|
356
|
+
id: 'deadline',
|
|
357
|
+
name: 'Deadline/Urgent',
|
|
358
|
+
category: EmailCategory.DEADLINE,
|
|
359
|
+
priority: 'critical',
|
|
360
|
+
patterns: {
|
|
361
|
+
subjectKeywords: ['deadline', 'due', 'expires', 'urgent', 'asap'],
|
|
362
|
+
bodyKeywords: ['deadline', 'due date', 'expires', 'urgent', 'immediately']
|
|
363
|
+
},
|
|
364
|
+
conditions: { matchType: 'any', minimumMatches: 1 }
|
|
365
|
+
},
|
|
366
|
+
// Invoices
|
|
367
|
+
{
|
|
368
|
+
id: 'invoice',
|
|
369
|
+
name: 'Invoice/Bill',
|
|
370
|
+
category: EmailCategory.INVOICE,
|
|
371
|
+
priority: 'medium',
|
|
372
|
+
patterns: {
|
|
373
|
+
subjectKeywords: ['invoice', 'bill', 'payment', 'receipt'],
|
|
374
|
+
bodyKeywords: ['invoice number', 'amount due', 'payment', 'bill']
|
|
375
|
+
},
|
|
376
|
+
conditions: { matchType: 'any', minimumMatches: 1 }
|
|
377
|
+
},
|
|
378
|
+
// Security alerts
|
|
379
|
+
{
|
|
380
|
+
id: 'security',
|
|
381
|
+
name: 'Security Alert',
|
|
382
|
+
category: EmailCategory.SECURITY,
|
|
383
|
+
priority: 'critical',
|
|
384
|
+
patterns: {
|
|
385
|
+
subjectKeywords: ['security', 'alert', 'breach', 'unauthorized', 'suspicious'],
|
|
386
|
+
bodyKeywords: ['security', 'breach', 'unauthorized', 'login', 'password']
|
|
387
|
+
},
|
|
388
|
+
conditions: { matchType: 'any', minimumMatches: 1 }
|
|
389
|
+
}
|
|
390
|
+
];
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { logger } from './api.js';
|
|
2
|
+
export class RateLimiter {
|
|
3
|
+
constructor(config) {
|
|
4
|
+
this.config = config;
|
|
5
|
+
this.requests = [];
|
|
6
|
+
this.throttleUntil = 0;
|
|
7
|
+
this.adaptiveDelay = 0;
|
|
8
|
+
this.consecutiveThrottles = 0;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Check if request is allowed
|
|
12
|
+
*/
|
|
13
|
+
checkLimit(endpoint) {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
const windowStart = now - this.config.windowMs;
|
|
16
|
+
// Remove old requests outside the window
|
|
17
|
+
this.requests = this.requests.filter(req => req.timestamp > windowStart);
|
|
18
|
+
// Check if we're still throttled
|
|
19
|
+
if (this.throttleUntil > now) {
|
|
20
|
+
return {
|
|
21
|
+
allowed: false,
|
|
22
|
+
remainingRequests: 0,
|
|
23
|
+
resetTime: this.throttleUntil,
|
|
24
|
+
retryAfter: this.throttleUntil - now,
|
|
25
|
+
isThrottled: true
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// Check burst allowance
|
|
29
|
+
const burstLimit = this.config.burstAllowance || Math.floor(this.config.maxRequests * 0.5);
|
|
30
|
+
const recentRequests = this.requests.filter(req => req.timestamp > now - 10000); // Last 10 seconds
|
|
31
|
+
if (recentRequests.length >= burstLimit) {
|
|
32
|
+
const retryAfter = 10000 - (now - recentRequests[0].timestamp);
|
|
33
|
+
return {
|
|
34
|
+
allowed: false,
|
|
35
|
+
remainingRequests: this.config.maxRequests - this.requests.length,
|
|
36
|
+
resetTime: windowStart + this.config.windowMs,
|
|
37
|
+
retryAfter,
|
|
38
|
+
isThrottled: false
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// Check window limit
|
|
42
|
+
if (this.requests.length >= this.config.maxRequests) {
|
|
43
|
+
const oldestRequest = this.requests[0];
|
|
44
|
+
const retryAfter = (oldestRequest.timestamp + this.config.windowMs) - now;
|
|
45
|
+
return {
|
|
46
|
+
allowed: false,
|
|
47
|
+
remainingRequests: 0,
|
|
48
|
+
resetTime: oldestRequest.timestamp + this.config.windowMs,
|
|
49
|
+
retryAfter,
|
|
50
|
+
isThrottled: false
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// Request is allowed - add to tracking
|
|
54
|
+
this.requests.push({ timestamp: now, endpoint });
|
|
55
|
+
return {
|
|
56
|
+
allowed: true,
|
|
57
|
+
remainingRequests: this.config.maxRequests - this.requests.length,
|
|
58
|
+
resetTime: windowStart + this.config.windowMs,
|
|
59
|
+
isThrottled: false
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Record a throttling event from the API
|
|
64
|
+
*/
|
|
65
|
+
recordThrottle(retryAfterMs) {
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
this.throttleUntil = now + retryAfterMs;
|
|
68
|
+
this.consecutiveThrottles++;
|
|
69
|
+
// Adaptive throttling - increase delay with consecutive throttles
|
|
70
|
+
if (this.config.adaptiveThrottling) {
|
|
71
|
+
this.adaptiveDelay = Math.min(this.consecutiveThrottles * 1000, 30000); // Max 30 seconds
|
|
72
|
+
logger.log(`🚨 Throttled by API. Consecutive throttles: ${this.consecutiveThrottles}, adaptive delay: ${this.adaptiveDelay}ms`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Record a successful request
|
|
77
|
+
*/
|
|
78
|
+
recordSuccess() {
|
|
79
|
+
// Reset consecutive throttles on success
|
|
80
|
+
if (this.consecutiveThrottles > 0) {
|
|
81
|
+
this.consecutiveThrottles = 0;
|
|
82
|
+
this.adaptiveDelay = 0;
|
|
83
|
+
logger.log('✅ API throttling recovered');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get current status
|
|
88
|
+
*/
|
|
89
|
+
getStatus() {
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
const windowStart = now - this.config.windowMs;
|
|
92
|
+
const activeRequests = this.requests.filter(req => req.timestamp > windowStart);
|
|
93
|
+
return {
|
|
94
|
+
requestsInWindow: activeRequests.length,
|
|
95
|
+
maxRequests: this.config.maxRequests,
|
|
96
|
+
isThrottled: this.throttleUntil > now,
|
|
97
|
+
throttleUntil: this.throttleUntil,
|
|
98
|
+
consecutiveThrottles: this.consecutiveThrottles,
|
|
99
|
+
adaptiveDelay: this.adaptiveDelay
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export class MS365RateLimitManager {
|
|
104
|
+
constructor() {
|
|
105
|
+
this.rateLimiters = new Map();
|
|
106
|
+
}
|
|
107
|
+
static getInstance() {
|
|
108
|
+
if (!this.instance) {
|
|
109
|
+
this.instance = new MS365RateLimitManager();
|
|
110
|
+
}
|
|
111
|
+
return this.instance;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get rate limiter for endpoint
|
|
115
|
+
*/
|
|
116
|
+
getRateLimiter(endpoint) {
|
|
117
|
+
if (!this.rateLimiters.has(endpoint)) {
|
|
118
|
+
const config = MS365RateLimitManager.ENDPOINT_CONFIGS[endpoint] ||
|
|
119
|
+
MS365RateLimitManager.ENDPOINT_CONFIGS['default'];
|
|
120
|
+
this.rateLimiters.set(endpoint, new RateLimiter(config));
|
|
121
|
+
}
|
|
122
|
+
return this.rateLimiters.get(endpoint);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Check if request is allowed
|
|
126
|
+
*/
|
|
127
|
+
checkRequest(endpoint, operation) {
|
|
128
|
+
const limiter = this.getRateLimiter(endpoint);
|
|
129
|
+
const result = limiter.checkLimit(operation);
|
|
130
|
+
if (!result.allowed) {
|
|
131
|
+
logger.log(`🚫 Rate limit exceeded for ${endpoint}:${operation}. Retry after ${result.retryAfter}ms`);
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Record a throttling event
|
|
137
|
+
*/
|
|
138
|
+
recordThrottle(endpoint, retryAfterMs) {
|
|
139
|
+
const limiter = this.getRateLimiter(endpoint);
|
|
140
|
+
limiter.recordThrottle(retryAfterMs);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Record a successful request
|
|
144
|
+
*/
|
|
145
|
+
recordSuccess(endpoint) {
|
|
146
|
+
const limiter = this.getRateLimiter(endpoint);
|
|
147
|
+
limiter.recordSuccess();
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Execute operation with rate limiting
|
|
151
|
+
*/
|
|
152
|
+
async executeWithRateLimit(operation, endpoint, operationName) {
|
|
153
|
+
const rateLimitResult = this.checkRequest(endpoint, operationName);
|
|
154
|
+
if (!rateLimitResult.allowed) {
|
|
155
|
+
const retryAfter = rateLimitResult.retryAfter || 1000;
|
|
156
|
+
logger.log(`⏳ Rate limited, waiting ${retryAfter}ms before ${endpoint}:${operationName}`);
|
|
157
|
+
await new Promise(resolve => setTimeout(resolve, retryAfter));
|
|
158
|
+
// Retry after waiting
|
|
159
|
+
return this.executeWithRateLimit(operation, endpoint, operationName);
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const result = await operation();
|
|
163
|
+
this.recordSuccess(endpoint);
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
// Check if error is due to rate limiting
|
|
168
|
+
if (error.status === 429 || error.code === 'TooManyRequests') {
|
|
169
|
+
const retryAfter = this.extractRetryAfter(error);
|
|
170
|
+
this.recordThrottle(endpoint, retryAfter);
|
|
171
|
+
logger.log(`🚨 API throttled ${endpoint}:${operationName}, waiting ${retryAfter}ms`);
|
|
172
|
+
await new Promise(resolve => setTimeout(resolve, retryAfter));
|
|
173
|
+
// Retry after throttling
|
|
174
|
+
return this.executeWithRateLimit(operation, endpoint, operationName);
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Extract retry-after value from error
|
|
181
|
+
*/
|
|
182
|
+
extractRetryAfter(error) {
|
|
183
|
+
// Check for Retry-After header
|
|
184
|
+
if (error.headers?.['retry-after']) {
|
|
185
|
+
return parseInt(error.headers['retry-after']) * 1000;
|
|
186
|
+
}
|
|
187
|
+
// Check for retry-after in error message
|
|
188
|
+
const retryMatch = error.message?.match(/retry after (\d+)/i);
|
|
189
|
+
if (retryMatch) {
|
|
190
|
+
return parseInt(retryMatch[1]) * 1000;
|
|
191
|
+
}
|
|
192
|
+
// Default retry after 60 seconds
|
|
193
|
+
return 60000;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Get status of all rate limiters
|
|
197
|
+
*/
|
|
198
|
+
getStatus() {
|
|
199
|
+
const status = {};
|
|
200
|
+
for (const [endpoint, limiter] of this.rateLimiters) {
|
|
201
|
+
status[endpoint] = limiter.getStatus();
|
|
202
|
+
}
|
|
203
|
+
return status;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Reset rate limiters (for testing or recovery)
|
|
207
|
+
*/
|
|
208
|
+
reset() {
|
|
209
|
+
this.rateLimiters.clear();
|
|
210
|
+
logger.log('🔄 Rate limiters reset');
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Get intelligent delay based on current state
|
|
214
|
+
*/
|
|
215
|
+
getIntelligentDelay(endpoint) {
|
|
216
|
+
const limiter = this.getRateLimiter(endpoint);
|
|
217
|
+
const status = limiter.getStatus();
|
|
218
|
+
// Base delay increases with request density
|
|
219
|
+
const requestDensity = status.requestsInWindow / status.maxRequests;
|
|
220
|
+
let baseDelay = 0;
|
|
221
|
+
if (requestDensity > 0.8) {
|
|
222
|
+
baseDelay = 2000; // 2 seconds when near limit
|
|
223
|
+
}
|
|
224
|
+
else if (requestDensity > 0.6) {
|
|
225
|
+
baseDelay = 1000; // 1 second when getting busy
|
|
226
|
+
}
|
|
227
|
+
else if (requestDensity > 0.4) {
|
|
228
|
+
baseDelay = 500; // 0.5 seconds when moderately busy
|
|
229
|
+
}
|
|
230
|
+
// Add adaptive delay if we've been throttled
|
|
231
|
+
const totalDelay = baseDelay + status.adaptiveDelay;
|
|
232
|
+
if (totalDelay > 0) {
|
|
233
|
+
logger.log(`🕐 Intelligent delay: ${totalDelay}ms for ${endpoint} (density: ${(requestDensity * 100).toFixed(1)}%)`);
|
|
234
|
+
}
|
|
235
|
+
return totalDelay;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Rate limit configurations for different MS365 endpoints
|
|
239
|
+
MS365RateLimitManager.ENDPOINT_CONFIGS = {
|
|
240
|
+
'search': {
|
|
241
|
+
maxRequests: 10,
|
|
242
|
+
windowMs: 60000, // 1 minute
|
|
243
|
+
burstAllowance: 5,
|
|
244
|
+
adaptiveThrottling: true
|
|
245
|
+
},
|
|
246
|
+
'email': {
|
|
247
|
+
maxRequests: 100,
|
|
248
|
+
windowMs: 60000, // 1 minute
|
|
249
|
+
burstAllowance: 20,
|
|
250
|
+
adaptiveThrottling: true
|
|
251
|
+
},
|
|
252
|
+
'send': {
|
|
253
|
+
maxRequests: 30,
|
|
254
|
+
windowMs: 60000, // 1 minute
|
|
255
|
+
burstAllowance: 10,
|
|
256
|
+
adaptiveThrottling: true
|
|
257
|
+
},
|
|
258
|
+
'attachment': {
|
|
259
|
+
maxRequests: 50,
|
|
260
|
+
windowMs: 60000, // 1 minute
|
|
261
|
+
burstAllowance: 10,
|
|
262
|
+
adaptiveThrottling: true
|
|
263
|
+
},
|
|
264
|
+
'calendar': {
|
|
265
|
+
maxRequests: 60,
|
|
266
|
+
windowMs: 60000, // 1 minute
|
|
267
|
+
burstAllowance: 15,
|
|
268
|
+
adaptiveThrottling: true
|
|
269
|
+
},
|
|
270
|
+
'contacts': {
|
|
271
|
+
maxRequests: 60,
|
|
272
|
+
windowMs: 60000, // 1 minute
|
|
273
|
+
burstAllowance: 15,
|
|
274
|
+
adaptiveThrottling: true
|
|
275
|
+
},
|
|
276
|
+
'default': {
|
|
277
|
+
maxRequests: 50,
|
|
278
|
+
windowMs: 60000, // 1 minute
|
|
279
|
+
burstAllowance: 10,
|
|
280
|
+
adaptiveThrottling: true
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
// Export singleton instance
|
|
284
|
+
export const ms365RateLimit = MS365RateLimitManager.getInstance();
|