UNPKG

ms365-mcp-server

Version:

Microsoft 365 MCP Server for managing Microsoft 365 email through natural language interactions with full OAuth2 authentication support

353 lines (352 loc) 14.2 kB
import { logger } from './api.js'; export class CrossReferenceDetector { constructor(ms365Operations) { this.ms365Operations = ms365Operations; } /** * Find all emails related to a specific email */ async findRelatedEmails(targetEmail, allEmails, options = {}) { const opts = { ...CrossReferenceDetector.DEFAULT_OPTIONS, ...options }; const results = []; logger.log(`🔍 Finding related emails for: ${targetEmail.subject}`); // Filter emails by time window const timeFilteredEmails = this.filterByTimeWindow(allEmails, targetEmail, opts.timeWindowDays); // 1. Find conversation thread emails if (opts.includeConversationThreads) { const conversationEmails = this.findConversationThreads(targetEmail, timeFilteredEmails); if (conversationEmails.length > 0) { results.push({ originalEmail: targetEmail, relatedEmails: conversationEmails, relationshipType: 'conversation', confidence: 0.95, reason: `Found ${conversationEmails.length} emails in same conversation thread` }); } } // 2. Find forwarded chains if (opts.includeForwardedChains) { const forwardedEmails = this.findForwardedChains(targetEmail, timeFilteredEmails); if (forwardedEmails.length > 0) { results.push({ originalEmail: targetEmail, relatedEmails: forwardedEmails, relationshipType: 'forwarded', confidence: 0.9, reason: `Found ${forwardedEmails.length} emails in forwarded chain` }); } } // 3. Find emails with references if (opts.includeReferences) { const referencedEmails = this.findReferencedEmails(targetEmail, timeFilteredEmails); if (referencedEmails.length > 0) { results.push({ originalEmail: targetEmail, relatedEmails: referencedEmails, relationshipType: 'reference', confidence: 0.85, reason: `Found ${referencedEmails.length} emails with cross-references` }); } } // 4. Find content similarity if (opts.includeContentSimilarity) { const similarEmails = this.findSimilarContent(targetEmail, timeFilteredEmails, opts.similarityThreshold); if (similarEmails.length > 0) { results.push({ originalEmail: targetEmail, relatedEmails: similarEmails, relationshipType: 'similarity', confidence: 0.8, reason: `Found ${similarEmails.length} emails with similar content` }); } } // Remove duplicates and limit results const uniqueResults = this.removeDuplicateResults(results); return uniqueResults.slice(0, opts.maxResults); } /** * Find emails in the same conversation thread */ findConversationThreads(targetEmail, emails) { return emails.filter(email => email.id !== targetEmail.id && email.conversationId === targetEmail.conversationId); } /** * Find forwarded email chains using subject patterns and content analysis */ findForwardedChains(targetEmail, emails) { const forwardedEmails = []; // Look for forwarded subject patterns const subjectPatterns = [ /^(fw|fwd|forward):\s*/i, /^re:\s*fw:/i, /^re:\s*fwd:/i ]; const cleanSubject = this.cleanSubject(targetEmail.subject); for (const email of emails) { if (email.id === targetEmail.id) continue; const emailCleanSubject = this.cleanSubject(email.subject); // Check if subjects match after cleaning if (this.subjectsMatch(cleanSubject, emailCleanSubject)) { // Check for forwarded patterns in body if (this.hasForwardedContent(email.bodyPreview, targetEmail.bodyPreview)) { forwardedEmails.push(email); } } } return forwardedEmails; } /** * Find emails with cross-references (mentions, attachments, etc.) */ findReferencedEmails(targetEmail, emails) { const referencedEmails = []; // Extract reference patterns from target email const referencePatterns = this.extractReferencePatterns(targetEmail); for (const email of emails) { if (email.id === targetEmail.id) continue; // Check if this email contains any reference patterns if (this.containsReferences(email, referencePatterns)) { referencedEmails.push(email); } } return referencedEmails; } /** * Find emails with similar content using various similarity metrics */ findSimilarContent(targetEmail, emails, threshold) { const similarEmails = []; const targetContent = this.extractContentFeatures(targetEmail); for (const email of emails) { if (email.id === targetEmail.id) continue; const emailContent = this.extractContentFeatures(email); const similarity = this.calculateContentSimilarity(targetContent, emailContent); if (similarity >= threshold) { similarEmails.push(email); } } return similarEmails; } /** * Extract content features for similarity comparison */ extractContentFeatures(email) { const text = `${email.subject} ${email.bodyPreview}`.toLowerCase(); return { keywords: this.extractKeywords(text), entities: this.extractEntities(text), subjects: [email.subject.toLowerCase()], senders: [email.from.address.toLowerCase(), email.from.name.toLowerCase()] }; } /** * Extract keywords from text */ extractKeywords(text) { // Remove common stop words and extract meaningful keywords 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']); const words = text.match(/\b\w{3,}\b/g) || []; return words.filter(word => !stopWords.has(word.toLowerCase())); } /** * Extract entities (emails, dates, numbers, etc.) */ extractEntities(text) { const entities = []; // Extract email addresses const emailRegex = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g; const emails = text.match(emailRegex) || []; entities.push(...emails); // Extract dates const dateRegex = /\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b/g; const dates = text.match(dateRegex) || []; entities.push(...dates); // Extract numbers that might be reference numbers const numberRegex = /\b\d{4,}\b/g; const numbers = text.match(numberRegex) || []; entities.push(...numbers); return entities; } /** * Calculate content similarity between two emails */ calculateContentSimilarity(content1, content2) { let totalScore = 0; let maxScore = 0; // Keyword similarity const keywordSimilarity = this.calculateArraySimilarity(content1.keywords, content2.keywords); totalScore += keywordSimilarity * 0.4; maxScore += 0.4; // Entity similarity const entitySimilarity = this.calculateArraySimilarity(content1.entities, content2.entities); totalScore += entitySimilarity * 0.3; maxScore += 0.3; // Subject similarity const subjectSimilarity = this.calculateArraySimilarity(content1.subjects, content2.subjects); totalScore += subjectSimilarity * 0.2; maxScore += 0.2; // Sender similarity const senderSimilarity = this.calculateArraySimilarity(content1.senders, content2.senders); totalScore += senderSimilarity * 0.1; maxScore += 0.1; return maxScore > 0 ? totalScore / maxScore : 0; } /** * Calculate similarity between two arrays of strings */ calculateArraySimilarity(arr1, arr2) { if (arr1.length === 0 && arr2.length === 0) return 1; if (arr1.length === 0 || arr2.length === 0) return 0; const set1 = new Set(arr1); const set2 = new Set(arr2); const intersection = new Set([...set1].filter(x => set2.has(x))); const union = new Set([...set1, ...set2]); return intersection.size / union.size; } /** * Clean subject line by removing prefixes and normalization */ cleanSubject(subject) { return subject .replace(/^(re|fw|fwd|forward):\s*/gi, '') .replace(/\s+/g, ' ') .trim() .toLowerCase(); } /** * Check if two subjects match after cleaning */ subjectsMatch(subject1, subject2) { return subject1 === subject2 || this.calculateStringSimilarity(subject1, subject2) > 0.8; } /** * Check if email contains forwarded content */ hasForwardedContent(bodyPreview1, bodyPreview2) { // Look for forwarded email patterns const forwardedPatterns = [ /from:\s*.*\s*sent:/i, /forwarded message/i, /original message/i, /---------- forwarded message/i ]; return forwardedPatterns.some(pattern => pattern.test(bodyPreview1) || pattern.test(bodyPreview2)); } /** * Extract reference patterns from an email */ extractReferencePatterns(email) { const patterns = []; const text = `${email.subject} ${email.bodyPreview}`; // Extract reference numbers, case numbers, etc. const refRegex = /\b(ref|reference|case|ticket|order|invoice|id)[\s#:]*([a-z0-9\-]+)/gi; const matches = text.match(refRegex) || []; patterns.push(...matches); return patterns; } /** * Check if email contains reference patterns */ containsReferences(email, patterns) { const text = `${email.subject} ${email.bodyPreview}`.toLowerCase(); return patterns.some(pattern => text.includes(pattern.toLowerCase())); } /** * Filter emails by time window */ filterByTimeWindow(emails, targetEmail, days) { const targetDate = new Date(targetEmail.receivedDateTime); const windowStart = new Date(targetDate.getTime() - (days * 24 * 60 * 60 * 1000)); const windowEnd = new Date(targetDate.getTime() + (days * 24 * 60 * 60 * 1000)); return emails.filter(email => { const emailDate = new Date(email.receivedDateTime); return emailDate >= windowStart && emailDate <= windowEnd; }); } /** * Remove duplicate results */ removeDuplicateResults(results) { const seen = new Set(); const uniqueResults = []; for (const result of results) { const key = `${result.relationshipType}-${result.relatedEmails.map(e => e.id).join(',')}`; if (!seen.has(key)) { seen.add(key); uniqueResults.push(result); } } return uniqueResults; } /** * Calculate string similarity using simple algorithm */ calculateStringSimilarity(str1, str2) { if (str1 === str2) return 1; const longer = str1.length > str2.length ? str1 : str2; const shorter = str1.length > str2.length ? str2 : str1; if (longer.length === 0) return 1; const editDistance = this.calculateEditDistance(longer, shorter); return (longer.length - editDistance) / longer.length; } /** * Calculate edit distance between two strings */ calculateEditDistance(str1, str2) { const matrix = Array(str2.length + 1).fill(null).map(() => Array(str1.length + 1).fill(null)); for (let i = 0; i <= str1.length; i++) matrix[0][i] = i; for (let j = 0; j <= str2.length; j++) matrix[j][0] = j; for (let j = 1; j <= str2.length; j++) { for (let i = 1; i <= str1.length; i++) { const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1; matrix[j][i] = Math.min(matrix[j][i - 1] + 1, matrix[j - 1][i] + 1, matrix[j - 1][i - 1] + indicator); } } return matrix[str2.length][str1.length]; } /** * Batch process multiple emails for cross-reference detection */ async findAllCrossReferences(emails, options = {}) { const results = new Map(); const opts = { ...CrossReferenceDetector.DEFAULT_OPTIONS, ...options }; logger.log(`🔍 Processing ${emails.length} emails for cross-references`); for (const email of emails) { try { const crossRefs = await this.findRelatedEmails(email, emails, opts); if (crossRefs.length > 0) { results.set(email.id, crossRefs); } } catch (error) { logger.error(`Error processing cross-references for email ${email.id}:`, error); } } logger.log(`🔍 Found cross-references for ${results.size} emails`); return results; } } CrossReferenceDetector.DEFAULT_OPTIONS = { includeConversationThreads: true, includeForwardedChains: true, includeContentSimilarity: true, includeReferences: true, similarityThreshold: 0.7, maxResults: 50, timeWindowDays: 365 };