UNPKG

msexchange-mcp

Version:

MCP server for Microsoft Exchange/Outlook email operations via Microsoft Graph API

370 lines 14.7 kB
import { MailService } from './mailService.js'; import { SearchQuerySanitizer } from './searchQuerySanitizer.js'; export class SmartQueryBuilder { userEmail; mailService; constructor(userEmail) { this.userEmail = userEmail; this.mailService = new MailService(userEmail); } /** * Analyzes the query options and determines the best strategy */ analyzeQuery(options) { // If user provided a raw filter, analyze its complexity if (options.filter && !options.search) { const complexity = this.assessFilterComplexity(options.filter); if (complexity === 'simple') { return { method: 'filter', query: options.filter, reason: 'Using provided filter (simple complexity)' }; } else { // Complex filter - try to simplify or provide fallback return { method: 'filter', query: this.simplifyFilter(options.filter), fallback: { method: 'search', query: this.extractSearchTermsFromFilter(options.filter), reason: 'Fallback to search if filter fails' }, reason: 'Simplified complex filter' }; } } // If user provided search, check if it needs sanitization if (options.search) { const searchStrategy = SearchQuerySanitizer.suggestSearchStrategy(options.search); if (searchStrategy.warnings.length > 0) { console.warn('Search query issues:', searchStrategy.warnings.join('; ')); } if (searchStrategy.strategy === 'filter' && searchStrategy.filterExpression) { return { method: 'filter', query: searchStrategy.filterExpression, reason: `Converted search to filter: ${searchStrategy.suggestions.join('; ')}` }; } else if (searchStrategy.strategy === 'hybrid') { return { method: 'search', query: searchStrategy.sanitizedQuery || options.search, fallback: searchStrategy.filterExpression ? { method: 'filter', query: searchStrategy.filterExpression, reason: 'Filter fallback for numeric content' } : undefined, reason: `Sanitized search: ${searchStrategy.suggestions.join('; ')}` }; } else { return { method: 'search', query: options.search, reason: 'Using search (orderBy not supported with search)' }; } } // Build optimal query from structured options if (options.from || options.to || options.subject || options.hasAttachments !== undefined || options.isRead !== undefined) { const filter = this.buildStructuredFilter(options); const complexity = this.assessFilterComplexity(filter); if (complexity === 'simple') { return { method: 'filter', query: filter, reason: 'Built simple filter from structured options' }; } else { // For complex queries, prefer search if we have text criteria const searchTerms = this.extractSearchableTerms(options); if (searchTerms) { return { method: 'search', query: searchTerms, fallback: { method: 'filter', query: this.buildSimplifiedFilter(options), reason: 'Simplified filter as fallback' }, reason: 'Using search for complex query' }; } return { method: 'filter', query: this.buildSimplifiedFilter(options), reason: 'Simplified complex filter' }; } } // No specific criteria - return recent emails return { method: 'filter', query: '', reason: 'No filter criteria - returning recent emails' }; } /** * Executes a query with automatic fallback handling */ async executeQuery(options) { const strategy = this.analyzeQuery(options); const warnings = []; let emails = []; let usedStrategy = strategy; try { emails = await this.executeQueryStrategy(strategy, options); } catch (error) { if (strategy.fallback && this.isRecoverableError(error)) { warnings.push(`Primary strategy failed: ${error.message}`); warnings.push(`Attempting fallback strategy...`); try { emails = await this.executeQueryStrategy(strategy.fallback, options); usedStrategy = strategy.fallback; warnings.push('Fallback strategy succeeded'); } catch (fallbackError) { throw new Error(`Both primary and fallback strategies failed. Primary: ${error.message}, Fallback: ${fallbackError.message}`); } } else { throw error; } } // Apply post-processing filters if needed if (usedStrategy.method === 'search' && this.needsPostFiltering(options)) { const beforeCount = emails.length; emails = this.applyPostFilters(emails, options); if (emails.length < beforeCount) { warnings.push(`Applied post-search filtering: ${beforeCount - emails.length} emails filtered out`); } } return { emails, strategy: `${usedStrategy.method}: ${usedStrategy.reason}`, warnings: warnings.length > 0 ? warnings : undefined }; } async executeQueryStrategy(strategy, options) { const queryOptions = { top: options.limit, skip: options.skip, select: this.getSelectFields(options.includeBody) }; if (strategy.method === 'search') { queryOptions.search = strategy.query; // Don't include orderBy with search } else { if (strategy.query) { queryOptions.filter = strategy.query; } if (options.orderBy) { queryOptions.orderBy = options.orderBy; } } // Handle folder filtering if (options.folder) { // Note: This would need to be implemented in mailService to support folder queries // For now, we'll add it to the filter if possible if (queryOptions.filter) { // Can't combine folder with existing filter easily in Graph API console.warn('Folder filtering with complex queries is limited in Graph API'); } } return await this.mailService.queryEmails(queryOptions); } assessFilterComplexity(filter) { // Simple filters have basic comparisons // Complex filters have multiple conditions, nested logic, or certain operators const complexIndicators = [ /\sand\s.*\sand\s/i, // Multiple AND conditions /\sor\s.*\sor\s/i, // Multiple OR conditions /contains\s*\([^)]*contains/i, // Nested contains /\([^)]*\([^)]*\)/, // Nested parentheses ]; for (const indicator of complexIndicators) { if (indicator.test(filter)) { return 'complex'; } } // Count operators const operatorCount = (filter.match(/\s(eq|ne|gt|lt|ge|le|contains|startswith|endswith)\s/gi) || []).length; return operatorCount > 2 ? 'complex' : 'simple'; } simplifyFilter(filter) { // Try to simplify complex filters by removing less important conditions // This is a basic implementation - could be made more sophisticated // If filter has OR conditions, take the first one if (filter.includes(' or ')) { const parts = filter.split(' or '); return parts[0].trim(); } // If filter has multiple AND conditions, prioritize key fields if (filter.includes(' and ')) { const parts = filter.split(' and '); // Prioritize from, subject, and date filters const priorityPart = parts.find(p => p.includes('from/emailAddress') || p.includes('subject') || p.includes('receivedDateTime')); return priorityPart || parts[0].trim(); } return filter; } extractSearchTermsFromFilter(filter) { // Extract searchable text from filter expressions const searchTerms = []; // Extract values from contains() expressions const containsMatches = filter.match(/contains\s*\(\s*[^,]+,\s*'([^']+)'\s*\)/g); if (containsMatches) { containsMatches.forEach(match => { const value = match.match(/'([^']+)'/); if (value) { searchTerms.push(value[1]); } }); } // Extract values from eq expressions const eqMatches = filter.match(/\beq\s+'([^']+)'/g); if (eqMatches) { eqMatches.forEach(match => { const value = match.match(/'([^']+)'/); if (value) { searchTerms.push(value[1]); } }); } return searchTerms.join(' '); } buildStructuredFilter(options) { const conditions = []; if (options.from) { // Try exact match first, can fall back to contains conditions.push(`from/emailAddress/address eq '${options.from}'`); } if (options.to) { conditions.push(`toRecipients/any(r: r/emailAddress/address eq '${options.to}')`); } if (options.subject) { conditions.push(`contains(subject, '${options.subject}')`); } if (options.hasAttachments !== undefined) { conditions.push(`hasAttachments eq ${options.hasAttachments}`); } if (options.isRead !== undefined) { conditions.push(`isRead eq ${options.isRead}`); } if (options.dateRange?.start) { conditions.push(`receivedDateTime ge ${options.dateRange.start}`); } if (options.dateRange?.end) { conditions.push(`receivedDateTime le ${options.dateRange.end}`); } return conditions.join(' and '); } buildSimplifiedFilter(options) { // Build a simpler filter that's less likely to fail const conditions = []; // Only include the most important criteria if (options.from) { // Use simpler from filter without nested property conditions.push(`from/emailAddress/address eq '${options.from}'`); } else if (options.subject) { conditions.push(`contains(subject, '${options.subject}')`); } // Add simple boolean filters if (options.hasAttachments !== undefined) { conditions.push(`hasAttachments eq ${options.hasAttachments}`); } return conditions.join(' and '); } extractSearchableTerms(options) { const terms = []; if (options.from) { // Extract domain or name from email const parts = options.from.split('@'); if (parts.length > 1) { // Add domain without TLD for better matching const domainParts = parts[1].split('.'); if (domainParts.length > 0) { terms.push(domainParts[0]); } } else { terms.push(options.from); } } if (options.subject) { terms.push(options.subject); } if (options.to) { const parts = options.to.split('@'); if (parts.length > 1) { terms.push(parts[0]); // username } } return terms.length > 0 ? terms.join(' ') : null; } isRecoverableError(error) { const recoverableMessages = [ 'restriction or sort order is too complex', 'is not supported with', 'query is too complex', 'invalid filter' ]; const errorMessage = error.message?.toLowerCase() || ''; return recoverableMessages.some(msg => errorMessage.includes(msg)); } needsPostFiltering(options) { // Check if we need to apply additional filters after search return !!(options.from || options.to || options.hasAttachments !== undefined || options.isRead !== undefined); } applyPostFilters(emails, options) { return emails.filter(email => { if (options.from && email.from?.emailAddress?.address !== options.from) { return false; } if (options.to) { const hasRecipient = email.toRecipients?.some(r => r.emailAddress.address === options.to); if (!hasRecipient) return false; } if (options.hasAttachments !== undefined && email.hasAttachments !== options.hasAttachments) { return false; } if (options.isRead !== undefined && email.isRead !== options.isRead) { return false; } return true; }); } getSelectFields(includeBody) { const fields = [ 'id', 'subject', 'from', 'toRecipients', 'ccRecipients', 'receivedDateTime', 'sentDateTime', 'hasAttachments', 'importance', 'isRead', 'isDraft', 'bodyPreview', 'categories', 'conversationId', ]; if (includeBody) { fields.push('body'); } return fields; } } //# sourceMappingURL=queryBuilder.js.map