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
JavaScript
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
};