Skip to main content
Glama

Github Project Manager

gmail-tools.ts15.1 kB
import { google } from 'googleapis'; import { OAuth2Client } from 'google-auth-library'; import { GmailApiError } from '../errors/gmail-api-error.js'; import { createEmailMessage } from '../utils/email.js'; import { extractEmailContent, extractAttachments } from '../utils/gmail.js'; import { GmailMessagePart } from '../types/gmail-message-part.js'; /** * Gmail tools implementation */ export class GmailTools { private readonly gmail; /** * Create a new GmailTools instance * * @param oauth2Client - The authenticated OAuth2 client */ constructor(private readonly oauth2Client: OAuth2Client) { this.gmail = google.gmail({ version: 'v1', auth: oauth2Client }); } /** * Handle send or draft email operations * * @param action - The action to perform ('send' or 'draft') * @param args - The validated arguments for the operation * @returns Result of the operation * @throws GmailApiError if the operation fails */ async handleEmailAction( action: 'send' | 'draft', args: { to: string[]; subject: string; body: string; cc?: string[]; bcc?: string[]; }, ) { try { const message = createEmailMessage(args); const encodedMessage = Buffer.from(message) .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); if (action === 'send') { const response = await this.gmail.users.messages.send({ userId: 'me', requestBody: { raw: encodedMessage, }, }); return { content: [ { type: 'text' as const, text: `Email sent successfully with ID: ${response.data.id}`, }, ], }; } else { const response = await this.gmail.users.drafts.create({ userId: 'me', requestBody: { message: { raw: encodedMessage, }, }, }); return { content: [ { type: 'text' as const, text: `Email draft created successfully with ID: ${response.data.id}`, }, ], }; } } catch (error) { throw new GmailApiError( `Failed to ${action} email: ${error instanceof Error ? error.message : String(error)}`, ); } } /** * Read a specific email * * @param messageId - The ID of the email to read * @returns The email content * @throws GmailApiError if the operation fails */ async readEmail(messageId: string) { try { const response = await this.gmail.users.messages.get({ userId: 'me', id: messageId, format: 'full', }); const headers = response.data.payload?.headers || []; const subject = headers.find((h) => h.name?.toLowerCase() === 'subject')?.value || ''; const from = headers.find((h) => h.name?.toLowerCase() === 'from')?.value || ''; const to = headers.find((h) => h.name?.toLowerCase() === 'to')?.value || ''; const date = headers.find((h) => h.name?.toLowerCase() === 'date')?.value || ''; // Extract email content const { text, html } = extractEmailContent((response.data.payload as GmailMessagePart) || {}); // Use plain text content if available, otherwise use HTML content const body = text || html || ''; // If we only have HTML content, add a note for the user const contentTypeNote = !text && html ? '[Note: This email is HTML-formatted. Plain text version not available.]\n\n' : ''; // Get attachment information const attachments = extractAttachments((response.data.payload as GmailMessagePart) || {}); // Add attachment info to output if any are present const attachmentInfo = attachments.length > 0 ? `\n\nAttachments (${attachments.length}):\n` + attachments .map((a) => `- ${a.filename} (${a.mimeType}, ${Math.round(a.size / 1024)} KB)`) .join('\n') : ''; return { content: [ { type: 'text' as const, text: `Subject: ${subject}\nFrom: ${from}\nTo: ${to}\nDate: ${date}\n\n${contentTypeNote}${body}${attachmentInfo}`, }, ], }; } catch (error) { throw new GmailApiError(`Failed to read email: ${error instanceof Error ? error.message : String(error)}`); } } /** * Search for emails using Gmail search syntax * * @param query - The search query * @param maxResults - Maximum number of results to return * @returns The search results * @throws GmailApiError if the operation fails */ async searchEmails(query: string, maxResults: number = 10) { try { const response = await this.gmail.users.messages.list({ userId: 'me', q: query, maxResults: maxResults, }); const messages = response.data.messages || []; const results = await Promise.all( messages.map(async (msg) => { const detail = await this.gmail.users.messages.get({ userId: 'me', id: msg.id!, format: 'metadata', metadataHeaders: ['Subject', 'From', 'Date'], }); const headers = detail.data.payload?.headers || []; return { id: msg.id, subject: headers.find((h) => h.name === 'Subject')?.value || '', from: headers.find((h) => h.name === 'From')?.value || '', date: headers.find((h) => h.name === 'Date')?.value || '', }; }), ); return { content: [ { type: 'text' as const, text: results .map((r) => `ID: ${r.id}\nSubject: ${r.subject}\nFrom: ${r.from}\nDate: ${r.date}\n`) .join('\n'), }, ], }; } catch (error) { throw new GmailApiError( `Failed to search emails: ${error instanceof Error ? error.message : String(error)}`, ); } } /** * Modify email labels * * @param messageId - The ID of the email to modify * @param labelIds - Label IDs to apply * @param addLabelIds - Label IDs to add * @param removeLabelIds - Label IDs to remove * @returns Result of the operation * @throws GmailApiError if the operation fails */ async modifyEmail(messageId: string, labelIds?: string[], addLabelIds?: string[], removeLabelIds?: string[]) { try { // Prepare request body const requestBody: { addLabelIds?: string[]; removeLabelIds?: string[] } = {}; if (labelIds) { requestBody.addLabelIds = labelIds; } if (addLabelIds) { requestBody.addLabelIds = addLabelIds; } if (removeLabelIds) { requestBody.removeLabelIds = removeLabelIds; } await this.gmail.users.messages.modify({ userId: 'me', id: messageId, requestBody: requestBody, }); return { content: [ { type: 'text' as const, text: `Email ${messageId} labels updated successfully`, }, ], }; } catch (error) { throw new GmailApiError( `Failed to modify email: ${error instanceof Error ? error.message : String(error)}`, ); } } /** * Delete an email * * @param messageId - The ID of the email to delete * @returns Result of the operation * @throws GmailApiError if the operation fails */ async deleteEmail(messageId: string) { try { await this.gmail.users.messages.delete({ userId: 'me', id: messageId, }); return { content: [ { type: 'text' as const, text: `Email ${messageId} deleted successfully`, }, ], }; } catch (error) { throw new GmailApiError( `Failed to delete email: ${error instanceof Error ? error.message : String(error)}`, ); } } /** * List email labels * * @returns The list of email labels * @throws GmailApiError if the operation fails */ async listEmailLabels() { try { const response = await this.gmail.users.labels.list({ userId: 'me', }); const labels = response.data.labels || []; const formattedLabels = labels.map((label) => ({ id: label.id, name: label.name, type: label.type, // Include additional useful information about each label messageListVisibility: label.messageListVisibility, labelListVisibility: label.labelListVisibility, // Only include count if it's a system label (as custom labels don't typically have counts) messagesTotal: label.messagesTotal, messagesUnread: label.messagesUnread, color: label.color, })); // Group labels by type (system vs user) for better organization const systemLabels = formattedLabels.filter((label) => label.type === 'system'); const userLabels = formattedLabels.filter((label) => label.type === 'user'); return { content: [ { type: 'text' as const, text: `Found ${labels.length} labels (${systemLabels.length} system, ${userLabels.length} user):\n\n` + 'System Labels:\n' + systemLabels.map((l) => `ID: ${l.id}\nName: ${l.name}\n`).join('\n') + '\nUser Labels:\n' + userLabels.map((l) => `ID: ${l.id}\nName: ${l.name}\n`).join('\n'), }, ], }; } catch (error) { throw new GmailApiError( `Failed to list email labels: ${error instanceof Error ? error.message : String(error)}`, ); } } /** * Search emails with advanced filtering options * * @param filters - Object containing various filter criteria * @returns The search results * @throws GmailApiError if the operation fails */ async searchWithFilters(filters: { from?: string; to?: string; subject?: string; afterDate?: string; beforeDate?: string; hasAttachment?: boolean; isRead?: boolean; isStarred?: boolean; inFolder?: string; hasWords?: string; doesNotHaveWords?: string; minSize?: number; maxSize?: number; labels?: string[]; maxResults?: number; }) { try { // Build Gmail search query based on provided filters const queryParts: string[] = []; // Sender filter if (filters.from) { queryParts.push(`from:${filters.from}`); } // Recipient filter if (filters.to) { queryParts.push(`to:${filters.to}`); } // Subject filter if (filters.subject) { queryParts.push(`subject:${filters.subject}`); } // Date range filters if (filters.afterDate) { queryParts.push(`after:${filters.afterDate}`); } if (filters.beforeDate) { queryParts.push(`before:${filters.beforeDate}`); } // Attachment filter if (filters.hasAttachment) { queryParts.push('has:attachment'); } // Read/unread status if (filters.isRead !== undefined) { queryParts.push(filters.isRead ? 'is:read' : 'is:unread'); } // Starred status if (filters.isStarred) { queryParts.push('is:starred'); } // Folder/location filter if (filters.inFolder) { queryParts.push(`in:${filters.inFolder}`); } // Content word filters if (filters.hasWords) { queryParts.push(filters.hasWords); // Simple word search doesn't need operator } // Negative content word filters if (filters.doesNotHaveWords) { queryParts.push(`-${filters.doesNotHaveWords}`); } // Size filters (convert to bytes) if (filters.minSize) { queryParts.push(`larger:${filters.minSize}m`); // Size in MB } if (filters.maxSize) { queryParts.push(`smaller:${filters.maxSize}m`); // Size in MB } // Label filters if (filters.labels && filters.labels.length > 0) { filters.labels.forEach((label) => { queryParts.push(`label:${label}`); }); } // Join all query parts with spaces const query = queryParts.join(' '); // Use existing search emails method with the constructed query return await this.searchEmails(query, filters.maxResults); } catch (error) { throw new GmailApiError( `Failed to search emails with filters: ${error instanceof Error ? error.message : String(error)}`, ); } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Monsoft-Solutions/model-context-protocols'

If you have feedback or need assistance with the MCP directory API, please join our Discord server