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