ms365-mcp-server 1.1.16 → 1.1.18

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,352 @@
1
+ import { logger } from './api.js';
2
+ export class CrossReferenceDetector {
3
+ constructor(ms365Operations) {
4
+ this.ms365Operations = ms365Operations;
5
+ }
6
+ /**
7
+ * Find all emails related to a specific email
8
+ */
9
+ async findRelatedEmails(targetEmail, allEmails, options = {}) {
10
+ const opts = { ...CrossReferenceDetector.DEFAULT_OPTIONS, ...options };
11
+ const results = [];
12
+ logger.log(`🔍 Finding related emails for: ${targetEmail.subject}`);
13
+ // Filter emails by time window
14
+ const timeFilteredEmails = this.filterByTimeWindow(allEmails, targetEmail, opts.timeWindowDays);
15
+ // 1. Find conversation thread emails
16
+ if (opts.includeConversationThreads) {
17
+ const conversationEmails = this.findConversationThreads(targetEmail, timeFilteredEmails);
18
+ if (conversationEmails.length > 0) {
19
+ results.push({
20
+ originalEmail: targetEmail,
21
+ relatedEmails: conversationEmails,
22
+ relationshipType: 'conversation',
23
+ confidence: 0.95,
24
+ reason: `Found ${conversationEmails.length} emails in same conversation thread`
25
+ });
26
+ }
27
+ }
28
+ // 2. Find forwarded chains
29
+ if (opts.includeForwardedChains) {
30
+ const forwardedEmails = this.findForwardedChains(targetEmail, timeFilteredEmails);
31
+ if (forwardedEmails.length > 0) {
32
+ results.push({
33
+ originalEmail: targetEmail,
34
+ relatedEmails: forwardedEmails,
35
+ relationshipType: 'forwarded',
36
+ confidence: 0.9,
37
+ reason: `Found ${forwardedEmails.length} emails in forwarded chain`
38
+ });
39
+ }
40
+ }
41
+ // 3. Find emails with references
42
+ if (opts.includeReferences) {
43
+ const referencedEmails = this.findReferencedEmails(targetEmail, timeFilteredEmails);
44
+ if (referencedEmails.length > 0) {
45
+ results.push({
46
+ originalEmail: targetEmail,
47
+ relatedEmails: referencedEmails,
48
+ relationshipType: 'reference',
49
+ confidence: 0.85,
50
+ reason: `Found ${referencedEmails.length} emails with cross-references`
51
+ });
52
+ }
53
+ }
54
+ // 4. Find content similarity
55
+ if (opts.includeContentSimilarity) {
56
+ const similarEmails = this.findSimilarContent(targetEmail, timeFilteredEmails, opts.similarityThreshold);
57
+ if (similarEmails.length > 0) {
58
+ results.push({
59
+ originalEmail: targetEmail,
60
+ relatedEmails: similarEmails,
61
+ relationshipType: 'similarity',
62
+ confidence: 0.8,
63
+ reason: `Found ${similarEmails.length} emails with similar content`
64
+ });
65
+ }
66
+ }
67
+ // Remove duplicates and limit results
68
+ const uniqueResults = this.removeDuplicateResults(results);
69
+ return uniqueResults.slice(0, opts.maxResults);
70
+ }
71
+ /**
72
+ * Find emails in the same conversation thread
73
+ */
74
+ findConversationThreads(targetEmail, emails) {
75
+ return emails.filter(email => email.id !== targetEmail.id &&
76
+ email.conversationId === targetEmail.conversationId);
77
+ }
78
+ /**
79
+ * Find forwarded email chains using subject patterns and content analysis
80
+ */
81
+ findForwardedChains(targetEmail, emails) {
82
+ const forwardedEmails = [];
83
+ // Look for forwarded subject patterns
84
+ const subjectPatterns = [
85
+ /^(fw|fwd|forward):\s*/i,
86
+ /^re:\s*fw:/i,
87
+ /^re:\s*fwd:/i
88
+ ];
89
+ const cleanSubject = this.cleanSubject(targetEmail.subject);
90
+ for (const email of emails) {
91
+ if (email.id === targetEmail.id)
92
+ continue;
93
+ const emailCleanSubject = this.cleanSubject(email.subject);
94
+ // Check if subjects match after cleaning
95
+ if (this.subjectsMatch(cleanSubject, emailCleanSubject)) {
96
+ // Check for forwarded patterns in body
97
+ if (this.hasForwardedContent(email.bodyPreview, targetEmail.bodyPreview)) {
98
+ forwardedEmails.push(email);
99
+ }
100
+ }
101
+ }
102
+ return forwardedEmails;
103
+ }
104
+ /**
105
+ * Find emails with cross-references (mentions, attachments, etc.)
106
+ */
107
+ findReferencedEmails(targetEmail, emails) {
108
+ const referencedEmails = [];
109
+ // Extract reference patterns from target email
110
+ const referencePatterns = this.extractReferencePatterns(targetEmail);
111
+ for (const email of emails) {
112
+ if (email.id === targetEmail.id)
113
+ continue;
114
+ // Check if this email contains any reference patterns
115
+ if (this.containsReferences(email, referencePatterns)) {
116
+ referencedEmails.push(email);
117
+ }
118
+ }
119
+ return referencedEmails;
120
+ }
121
+ /**
122
+ * Find emails with similar content using various similarity metrics
123
+ */
124
+ findSimilarContent(targetEmail, emails, threshold) {
125
+ const similarEmails = [];
126
+ const targetContent = this.extractContentFeatures(targetEmail);
127
+ for (const email of emails) {
128
+ if (email.id === targetEmail.id)
129
+ continue;
130
+ const emailContent = this.extractContentFeatures(email);
131
+ const similarity = this.calculateContentSimilarity(targetContent, emailContent);
132
+ if (similarity >= threshold) {
133
+ similarEmails.push(email);
134
+ }
135
+ }
136
+ return similarEmails;
137
+ }
138
+ /**
139
+ * Extract content features for similarity comparison
140
+ */
141
+ extractContentFeatures(email) {
142
+ const text = `${email.subject} ${email.bodyPreview}`.toLowerCase();
143
+ return {
144
+ keywords: this.extractKeywords(text),
145
+ entities: this.extractEntities(text),
146
+ subjects: [email.subject.toLowerCase()],
147
+ senders: [email.from.address.toLowerCase(), email.from.name.toLowerCase()]
148
+ };
149
+ }
150
+ /**
151
+ * Extract keywords from text
152
+ */
153
+ extractKeywords(text) {
154
+ // Remove common stop words and extract meaningful keywords
155
+ const stopWords = new Set(['the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'is', 'are', 'was', 'were', 'be', 'been', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'can', 'a', 'an', 'this', 'that', 'these', 'those']);
156
+ const words = text.match(/\b\w{3,}\b/g) || [];
157
+ return words.filter(word => !stopWords.has(word.toLowerCase()));
158
+ }
159
+ /**
160
+ * Extract entities (emails, dates, numbers, etc.)
161
+ */
162
+ extractEntities(text) {
163
+ const entities = [];
164
+ // Extract email addresses
165
+ const emailRegex = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g;
166
+ const emails = text.match(emailRegex) || [];
167
+ entities.push(...emails);
168
+ // Extract dates
169
+ const dateRegex = /\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b/g;
170
+ const dates = text.match(dateRegex) || [];
171
+ entities.push(...dates);
172
+ // Extract numbers that might be reference numbers
173
+ const numberRegex = /\b\d{4,}\b/g;
174
+ const numbers = text.match(numberRegex) || [];
175
+ entities.push(...numbers);
176
+ return entities;
177
+ }
178
+ /**
179
+ * Calculate content similarity between two emails
180
+ */
181
+ calculateContentSimilarity(content1, content2) {
182
+ let totalScore = 0;
183
+ let maxScore = 0;
184
+ // Keyword similarity
185
+ const keywordSimilarity = this.calculateArraySimilarity(content1.keywords, content2.keywords);
186
+ totalScore += keywordSimilarity * 0.4;
187
+ maxScore += 0.4;
188
+ // Entity similarity
189
+ const entitySimilarity = this.calculateArraySimilarity(content1.entities, content2.entities);
190
+ totalScore += entitySimilarity * 0.3;
191
+ maxScore += 0.3;
192
+ // Subject similarity
193
+ const subjectSimilarity = this.calculateArraySimilarity(content1.subjects, content2.subjects);
194
+ totalScore += subjectSimilarity * 0.2;
195
+ maxScore += 0.2;
196
+ // Sender similarity
197
+ const senderSimilarity = this.calculateArraySimilarity(content1.senders, content2.senders);
198
+ totalScore += senderSimilarity * 0.1;
199
+ maxScore += 0.1;
200
+ return maxScore > 0 ? totalScore / maxScore : 0;
201
+ }
202
+ /**
203
+ * Calculate similarity between two arrays of strings
204
+ */
205
+ calculateArraySimilarity(arr1, arr2) {
206
+ if (arr1.length === 0 && arr2.length === 0)
207
+ return 1;
208
+ if (arr1.length === 0 || arr2.length === 0)
209
+ return 0;
210
+ const set1 = new Set(arr1);
211
+ const set2 = new Set(arr2);
212
+ const intersection = new Set([...set1].filter(x => set2.has(x)));
213
+ const union = new Set([...set1, ...set2]);
214
+ return intersection.size / union.size;
215
+ }
216
+ /**
217
+ * Clean subject line by removing prefixes and normalization
218
+ */
219
+ cleanSubject(subject) {
220
+ return subject
221
+ .replace(/^(re|fw|fwd|forward):\s*/gi, '')
222
+ .replace(/\s+/g, ' ')
223
+ .trim()
224
+ .toLowerCase();
225
+ }
226
+ /**
227
+ * Check if two subjects match after cleaning
228
+ */
229
+ subjectsMatch(subject1, subject2) {
230
+ return subject1 === subject2 ||
231
+ this.calculateStringSimilarity(subject1, subject2) > 0.8;
232
+ }
233
+ /**
234
+ * Check if email contains forwarded content
235
+ */
236
+ hasForwardedContent(bodyPreview1, bodyPreview2) {
237
+ // Look for forwarded email patterns
238
+ const forwardedPatterns = [
239
+ /from:\s*.*\s*sent:/i,
240
+ /forwarded message/i,
241
+ /original message/i,
242
+ /---------- forwarded message/i
243
+ ];
244
+ return forwardedPatterns.some(pattern => pattern.test(bodyPreview1) || pattern.test(bodyPreview2));
245
+ }
246
+ /**
247
+ * Extract reference patterns from an email
248
+ */
249
+ extractReferencePatterns(email) {
250
+ const patterns = [];
251
+ const text = `${email.subject} ${email.bodyPreview}`;
252
+ // Extract reference numbers, case numbers, etc.
253
+ const refRegex = /\b(ref|reference|case|ticket|order|invoice|id)[\s#:]*([a-z0-9\-]+)/gi;
254
+ const matches = text.match(refRegex) || [];
255
+ patterns.push(...matches);
256
+ return patterns;
257
+ }
258
+ /**
259
+ * Check if email contains reference patterns
260
+ */
261
+ containsReferences(email, patterns) {
262
+ const text = `${email.subject} ${email.bodyPreview}`.toLowerCase();
263
+ return patterns.some(pattern => text.includes(pattern.toLowerCase()));
264
+ }
265
+ /**
266
+ * Filter emails by time window
267
+ */
268
+ filterByTimeWindow(emails, targetEmail, days) {
269
+ const targetDate = new Date(targetEmail.receivedDateTime);
270
+ const windowStart = new Date(targetDate.getTime() - (days * 24 * 60 * 60 * 1000));
271
+ const windowEnd = new Date(targetDate.getTime() + (days * 24 * 60 * 60 * 1000));
272
+ return emails.filter(email => {
273
+ const emailDate = new Date(email.receivedDateTime);
274
+ return emailDate >= windowStart && emailDate <= windowEnd;
275
+ });
276
+ }
277
+ /**
278
+ * Remove duplicate results
279
+ */
280
+ removeDuplicateResults(results) {
281
+ const seen = new Set();
282
+ const uniqueResults = [];
283
+ for (const result of results) {
284
+ const key = `${result.relationshipType}-${result.relatedEmails.map(e => e.id).join(',')}`;
285
+ if (!seen.has(key)) {
286
+ seen.add(key);
287
+ uniqueResults.push(result);
288
+ }
289
+ }
290
+ return uniqueResults;
291
+ }
292
+ /**
293
+ * Calculate string similarity using simple algorithm
294
+ */
295
+ calculateStringSimilarity(str1, str2) {
296
+ if (str1 === str2)
297
+ return 1;
298
+ const longer = str1.length > str2.length ? str1 : str2;
299
+ const shorter = str1.length > str2.length ? str2 : str1;
300
+ if (longer.length === 0)
301
+ return 1;
302
+ const editDistance = this.calculateEditDistance(longer, shorter);
303
+ return (longer.length - editDistance) / longer.length;
304
+ }
305
+ /**
306
+ * Calculate edit distance between two strings
307
+ */
308
+ calculateEditDistance(str1, str2) {
309
+ const matrix = Array(str2.length + 1).fill(null).map(() => Array(str1.length + 1).fill(null));
310
+ for (let i = 0; i <= str1.length; i++)
311
+ matrix[0][i] = i;
312
+ for (let j = 0; j <= str2.length; j++)
313
+ matrix[j][0] = j;
314
+ for (let j = 1; j <= str2.length; j++) {
315
+ for (let i = 1; i <= str1.length; i++) {
316
+ const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1;
317
+ matrix[j][i] = Math.min(matrix[j][i - 1] + 1, matrix[j - 1][i] + 1, matrix[j - 1][i - 1] + indicator);
318
+ }
319
+ }
320
+ return matrix[str2.length][str1.length];
321
+ }
322
+ /**
323
+ * Batch process multiple emails for cross-reference detection
324
+ */
325
+ async findAllCrossReferences(emails, options = {}) {
326
+ const results = new Map();
327
+ const opts = { ...CrossReferenceDetector.DEFAULT_OPTIONS, ...options };
328
+ logger.log(`🔍 Processing ${emails.length} emails for cross-references`);
329
+ for (const email of emails) {
330
+ try {
331
+ const crossRefs = await this.findRelatedEmails(email, emails, opts);
332
+ if (crossRefs.length > 0) {
333
+ results.set(email.id, crossRefs);
334
+ }
335
+ }
336
+ catch (error) {
337
+ logger.error(`Error processing cross-references for email ${email.id}:`, error);
338
+ }
339
+ }
340
+ logger.log(`🔍 Found cross-references for ${results.size} emails`);
341
+ return results;
342
+ }
343
+ }
344
+ CrossReferenceDetector.DEFAULT_OPTIONS = {
345
+ includeConversationThreads: true,
346
+ includeForwardedChains: true,
347
+ includeContentSimilarity: true,
348
+ includeReferences: true,
349
+ similarityThreshold: 0.7,
350
+ maxResults: 50,
351
+ timeWindowDays: 365
352
+ };