msexchange-mcp
Version:
MCP server for Microsoft Exchange/Outlook email operations via Microsoft Graph API
213 lines • 9.99 kB
JavaScript
import { getGraphClient, executeWithTokenRetry } from '../graph/graphClient.js';
export class MailService {
userEmail;
constructor(userEmail) {
this.userEmail = userEmail;
}
async retryWithBackoff(operation, maxRetries = 3, baseDelay = 1000) {
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
}
catch (error) {
lastError = error;
// Check if it's a rate limiting error
if (error.code === 'TooManyRequests' || error.status === 429) {
const retryAfter = error.headers?.['retry-after'];
const delay = retryAfter ? parseInt(retryAfter) * 1000 : baseDelay * Math.pow(2, attempt);
console.error(`🚨 Rate limited on attempt ${attempt + 1}/${maxRetries + 1}, retrying after ${delay}ms`);
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
}
// If it's not a rate limiting error or we've exhausted retries, throw
throw error;
}
}
throw lastError;
}
async queryEmails(options) {
return this.retryWithBackoff(async () => {
return executeWithTokenRetry(this.userEmail, async (client) => {
let query = client.api(`/users/${this.userEmail}/messages`);
// Important: Microsoft Graph API restrictions:
// 1. Cannot use $search with $orderBy
// 2. Complex $filter expressions may not work with $orderBy
if (options.search) {
query = query.search(options.search);
// Log warning if orderBy is also specified
if (options.orderBy) {
console.error('⚠️ Warning: $orderBy is not supported with $search queries. Ignoring orderBy parameter.');
}
}
if (options.filter) {
query = query.filter(options.filter);
}
if (options.top) {
// Ensure we don't exceed Graph API limits
const safeTop = Math.min(options.top, 1000);
query = query.top(safeTop);
}
if (options.skip) {
query = query.skip(options.skip);
}
// Only apply orderBy if search is not being used
if (options.orderBy && !options.search) {
try {
query = query.orderby(options.orderBy);
}
catch (error) {
console.error(`⚠️ Warning: orderBy "${options.orderBy}" failed, possibly due to complex filter. Proceeding without sorting.`);
}
}
if (options.select && options.select.length > 0) {
query = query.select(options.select);
}
try {
const response = await query.get();
return response.value;
}
catch (error) {
// Handle specific Graph API errors
if (error.message?.includes('restriction or sort order is too complex')) {
console.error('⚠️ Graph API error: restriction or sort order is too complex. Retrying without orderBy...');
// Retry without orderBy
query = client.api(`/users/${this.userEmail}/messages`);
if (options.search) {
query = query.search(options.search);
}
if (options.filter) {
query = query.filter(options.filter);
}
if (options.top) {
const safeTop = Math.min(options.top, 1000);
query = query.top(safeTop);
}
if (options.skip) {
query = query.skip(options.skip);
}
if (options.select && options.select.length > 0) {
query = query.select(options.select);
}
const response = await query.get();
return response.value;
}
// Re-throw other errors
throw error;
}
});
});
}
async getEmailById(messageId) {
return this.retryWithBackoff(async () => {
const client = await getGraphClient(this.userEmail);
return await client.api(`/users/${this.userEmail}/messages/${messageId}`).get();
});
}
async moveEmail(messageId, destinationFolderId) {
return this.retryWithBackoff(async () => {
const client = await getGraphClient(this.userEmail);
return await client.api(`/users/${this.userEmail}/messages/${messageId}/move`)
.post({ destinationId: destinationFolderId });
});
}
async updateEmail(messageId, updates) {
return this.retryWithBackoff(async () => {
const client = await getGraphClient(this.userEmail);
return await client.api(`/users/${this.userEmail}/messages/${messageId}`)
.patch(updates);
});
}
async listFolders() {
return this.retryWithBackoff(async () => {
const client = await getGraphClient(this.userEmail);
const response = await client.api(`/users/${this.userEmail}/mailFolders`)
.select(['id', 'displayName', 'parentFolderId', 'childFolderCount', 'unreadItemCount', 'totalItemCount'])
.get();
return response.value;
});
}
async createFolder(folderData) {
return this.retryWithBackoff(async () => {
const client = await getGraphClient(this.userEmail);
// If no parent folder specified, create in the root mailFolders
const endpoint = folderData.parentFolderId
? `/users/${this.userEmail}/mailFolders/${folderData.parentFolderId}/childFolders`
: `/users/${this.userEmail}/mailFolders`;
return await client.api(endpoint).post({
displayName: folderData.displayName,
});
});
}
async createDraft(draft) {
return this.retryWithBackoff(async () => {
const client = await getGraphClient(this.userEmail);
return await client.api(`/users/${this.userEmail}/messages`).post(draft);
});
}
async addAttachmentToDraft(messageId, attachment) {
return this.retryWithBackoff(async () => {
const client = await getGraphClient(this.userEmail);
const attachmentData = {
'@odata.type': '#microsoft.graph.fileAttachment',
name: attachment.name,
contentType: attachment.contentType || 'application/octet-stream',
contentBytes: attachment.contentBytes,
};
// For large attachments (>4MB), we would need to use upload sessions
// For now, we'll handle smaller attachments directly
if (attachment.size && attachment.size > 4 * 1024 * 1024) {
throw new Error('Attachments larger than 4MB require upload sessions (not yet implemented)');
}
return await client.api(`/users/${this.userEmail}/messages/${messageId}/attachments`)
.post(attachmentData);
});
}
async sendMail(message, saveToSentItems = true) {
return this.retryWithBackoff(async () => {
const client = await getGraphClient(this.userEmail);
await client.api(`/users/${this.userEmail}/sendMail`)
.post({ message, saveToSentItems });
});
}
async deleteEmail(messageId) {
return this.retryWithBackoff(async () => {
const client = await getGraphClient(this.userEmail);
await client.api(`/users/${this.userEmail}/messages/${messageId}`).delete();
});
}
async listMailRules() {
return this.retryWithBackoff(async () => {
const client = await getGraphClient(this.userEmail);
const response = await client.api(`/users/${this.userEmail}/mailFolders/inbox/messageRules`).get();
return response.value;
});
}
async createMailRule(ruleData) {
return this.retryWithBackoff(async () => {
const client = await getGraphClient(this.userEmail);
return await client.api(`/users/${this.userEmail}/mailFolders/inbox/messageRules`).post(ruleData);
});
}
async getMailRule(ruleId) {
return this.retryWithBackoff(async () => {
const client = await getGraphClient(this.userEmail);
return await client.api(`/users/${this.userEmail}/mailFolders/inbox/messageRules/${ruleId}`).get();
});
}
async updateMailRule(ruleId, updateData) {
return this.retryWithBackoff(async () => {
const client = await getGraphClient(this.userEmail);
return await client.api(`/users/${this.userEmail}/mailFolders/inbox/messageRules/${ruleId}`).patch(updateData);
});
}
async deleteMailRule(ruleId) {
return this.retryWithBackoff(async () => {
const client = await getGraphClient(this.userEmail);
await client.api(`/users/${this.userEmail}/mailFolders/inbox/messageRules/${ruleId}`).delete();
});
}
}
//# sourceMappingURL=mailService.js.map