msexchange-mcp
Version:
MCP server for Microsoft Exchange/Outlook email operations via Microsoft Graph API
370 lines • 14.7 kB
JavaScript
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