UNPKG

gmail-mcp-server

Version:

Gmail MCP Server with on-demand authentication for SIYA/Claude Desktop. Complete Gmail integration with multi-user support and OAuth2 security.

310 lines 11.8 kB
import { google } from 'googleapis'; import { promises as fs } from 'fs'; import * as path from 'path'; import { LargeAttachmentHandler } from './attachment-handler.js'; export class GmailService { credentials; gmail = null; oauth2Client; attachmentHandler = null; SCOPES = [ 'https://www.googleapis.com/auth/gmail.readonly', 'https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/gmail.modify' ]; constructor(credentials) { this.credentials = credentials; this.oauth2Client = new google.auth.OAuth2(credentials.client_id, credentials.client_secret, credentials.redirect_uri); } async authenticate(authCode) { if (authCode) { // Exchange authorization code for tokens try { // Use the promisified version const getAccessTokenAsync = () => { return new Promise((resolve, reject) => { this.oauth2Client.getAccessToken(authCode, (error, tokens) => { if (error) reject(error); else resolve(tokens); }); }); }; const tokens = await getAccessTokenAsync(); if (!tokens) { throw new Error('No tokens received from OAuth2'); } this.oauth2Client.setCredentials(tokens); // Save tokens for future use await this.saveTokens(tokens); this.initializeServices(); return 'Authentication successful'; } catch (error) { throw new Error(`Authentication failed: ${error}`); } } else { // Generate auth URL const authUrl = this.oauth2Client.generateAuthUrl({ access_type: 'offline', scope: this.SCOPES, }); return authUrl; } } async loadSavedTokens() { try { const tokenPath = path.join(process.cwd(), 'tokens.json'); const tokens = JSON.parse(await fs.readFile(tokenPath, 'utf8')); this.oauth2Client.setCredentials(tokens); this.initializeServices(); return true; } catch (error) { return false; } } async saveTokens(tokens) { const tokenPath = path.join(process.cwd(), 'tokens.json'); await fs.writeFile(tokenPath, JSON.stringify(tokens, null, 2)); } initializeServices() { this.gmail = google.gmail({ version: 'v1', auth: this.oauth2Client }); this.attachmentHandler = new LargeAttachmentHandler(this.gmail); } ensureAuthenticated() { if (!this.gmail || !this.attachmentHandler) { throw new Error('Gmail service not authenticated. Please authenticate first.'); } } /** * Send email with improved attachment handling */ async sendEmail(message) { this.ensureAuthenticated(); try { let emailContent = this.buildEmailContent(message); if (message.attachments && message.attachments.length > 0) { // Process attachments const attachmentResults = await this.attachmentHandler.processAttachmentsForSending(message.attachments); // Validate all attachments for (const attachment of message.attachments) { const validation = this.attachmentHandler.validateAttachment(attachment); if (!validation.valid) { throw new Error(`Attachment validation failed: ${validation.error}`); } } // Create attachment parts const attachmentParts = await this.attachmentHandler.createAttachmentParts(attachmentResults); // Build multipart email with attachments emailContent = this.buildMultipartEmailWithAttachments(message, attachmentParts); // Cleanup temp files after sending setTimeout(() => { this.attachmentHandler.cleanup(attachmentResults); }, 5000); } const response = await this.gmail.users.messages.send({ userId: 'me', requestBody: { raw: Buffer.from(emailContent).toString('base64url') } }); return response.data.id || 'Message sent successfully'; } catch (error) { throw new Error(`Failed to send email: ${error}`); } } /** * Download attachment with streaming support */ async downloadAttachment(options) { this.ensureAuthenticated(); return this.attachmentHandler.downloadAttachment(options); } /** * Download attachment and save to local file system with auto-generated path */ async downloadAttachmentToLocal(messageId, attachmentId, customPath) { this.ensureAuthenticated(); return this.attachmentHandler.downloadAttachmentToLocal(messageId, attachmentId, customPath); } /** * Get attachment information without downloading */ async getAttachmentInfo(messageId, attachmentId) { this.ensureAuthenticated(); return this.attachmentHandler.getAttachmentInfo(messageId, attachmentId); } /** * Search emails with enhanced capabilities */ async searchEmails(query, maxResults = 50) { this.ensureAuthenticated(); try { const response = await this.gmail.users.messages.list({ userId: 'me', q: query, maxResults }); if (!response.data.messages) { return []; } // Get full message details const messages = await Promise.all(response.data.messages.map(async (msg) => { const fullMessage = await this.gmail.users.messages.get({ userId: 'me', id: msg.id, format: 'full' }); return fullMessage.data; })); return messages; } catch (error) { throw new Error(`Failed to search emails: ${error}`); } } /** * Read email by ID */ async readEmail(messageId, format = 'full') { this.ensureAuthenticated(); try { const response = await this.gmail.users.messages.get({ userId: 'me', id: messageId, format }); return response.data; } catch (error) { throw new Error(`Failed to read email: ${error}`); } } /** * List all downloaded attachments */ async listDownloadedAttachments(downloadsDir) { this.ensureAuthenticated(); return this.attachmentHandler.listDownloadedAttachments(downloadsDir); } /** * Clean up old downloaded attachments */ async cleanupOldDownloads(maxAgeMs, downloadsDir) { this.ensureAuthenticated(); return this.attachmentHandler.cleanupOldDownloads(maxAgeMs, downloadsDir); } /** * Get memory usage statistics */ getMemoryStats() { return this.attachmentHandler?.getMemoryStats() || { heapUsed: 0, heapTotal: 0, external: 0 }; } buildEmailContent(message) { const headers = [ `To: ${message.to.join(', ')}`, ...(message.cc ? [`Cc: ${message.cc.join(', ')}`] : []), ...(message.bcc ? [`Bcc: ${message.bcc.join(', ')}`] : []), `Subject: ${message.subject}`, ...(message.replyTo ? [`Reply-To: ${message.replyTo}`] : []), 'MIME-Version: 1.0' ]; if (message.html && message.text) { // Multipart alternative const boundary = `boundary_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; headers.push(`Content-Type: multipart/alternative; boundary="${boundary}"`); return [ headers.join('\r\n'), '', `--${boundary}`, 'Content-Type: text/plain; charset=utf-8', 'Content-Transfer-Encoding: 8bit', '', message.text, '', `--${boundary}`, 'Content-Type: text/html; charset=utf-8', 'Content-Transfer-Encoding: 8bit', '', message.html, '', `--${boundary}--` ].join('\r\n'); } else if (message.html) { headers.push('Content-Type: text/html; charset=utf-8'); headers.push('Content-Transfer-Encoding: 8bit'); return [headers.join('\r\n'), '', message.html].join('\r\n'); } else { headers.push('Content-Type: text/plain; charset=utf-8'); headers.push('Content-Transfer-Encoding: 8bit'); return [headers.join('\r\n'), '', message.text || ''].join('\r\n'); } } buildMultipartEmailWithAttachments(message, attachmentParts) { const boundary = `boundary_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const headers = [ `To: ${message.to.join(', ')}`, ...(message.cc ? [`Cc: ${message.cc.join(', ')}`] : []), ...(message.bcc ? [`Bcc: ${message.bcc.join(', ')}`] : []), `Subject: ${message.subject}`, ...(message.replyTo ? [`Reply-To: ${message.replyTo}`] : []), 'MIME-Version: 1.0', `Content-Type: multipart/mixed; boundary="${boundary}"` ]; const parts = [headers.join('\r\n'), '']; // Add text/html content parts.push(`--${boundary}`); if (message.html && message.text) { const altBoundary = `alt_boundary_${Date.now()}`; parts.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`); parts.push(''); parts.push(`--${altBoundary}`); parts.push('Content-Type: text/plain; charset=utf-8'); parts.push(''); parts.push(message.text); parts.push(''); parts.push(`--${altBoundary}`); parts.push('Content-Type: text/html; charset=utf-8'); parts.push(''); parts.push(message.html); parts.push(''); parts.push(`--${altBoundary}--`); } else if (message.html) { parts.push('Content-Type: text/html; charset=utf-8'); parts.push(''); parts.push(message.html); } else { parts.push('Content-Type: text/plain; charset=utf-8'); parts.push(''); parts.push(message.text || ''); } // Add attachments for (const attachmentPart of attachmentParts) { parts.push(''); parts.push(`--${boundary}`); // Add headers if (attachmentPart.headers) { for (const header of attachmentPart.headers) { parts.push(`${header.name}: ${header.value}`); } } parts.push(''); // Add attachment data if (attachmentPart.body?.data) { parts.push(attachmentPart.body.data); } } parts.push(''); parts.push(`--${boundary}--`); return parts.join('\r\n'); } } //# sourceMappingURL=gmail-service.js.map