mcp-memory-libsql

by spences10
Verified
import { google, gmail_v1 } from 'googleapis'; import { EmailResponse, GetEmailsParams, GetEmailsResponse, SendEmailParams, SendEmailResponse, ThreadInfo, GmailError, IncomingGmailAttachment, OutgoingGmailAttachment } from '../types.js'; import { SearchService } from './search.js'; import { GmailAttachmentService } from './attachment.js'; import { AttachmentResponseTransformer } from '../../attachments/response-transformer.js'; import { AttachmentIndexService } from '../../attachments/index-service.js'; type GmailMessage = gmail_v1.Schema$Message; export class EmailService { private responseTransformer: AttachmentResponseTransformer; constructor( private searchService: SearchService, private attachmentService: GmailAttachmentService, private gmailClient?: ReturnType<typeof google.gmail> ) { this.responseTransformer = new AttachmentResponseTransformer(AttachmentIndexService.getInstance()); } /** * Updates the Gmail client instance * @param client - New Gmail client instance */ updateClient(client: ReturnType<typeof google.gmail>) { this.gmailClient = client; } private ensureClient(): ReturnType<typeof google.gmail> { if (!this.gmailClient) { throw new GmailError( 'Gmail client not initialized', 'CLIENT_ERROR', 'Please ensure the service is initialized' ); } return this.gmailClient; } /** * Extracts all headers into a key-value map */ private extractHeaders(headers: { name: string; value: string }[]): { [key: string]: string } { return headers.reduce((acc, header) => { acc[header.name] = header.value; return acc; }, {} as { [key: string]: string }); } /** * Groups emails by thread ID and extracts thread information */ private groupEmailsByThread(emails: EmailResponse[]): { [threadId: string]: ThreadInfo } { return emails.reduce((threads, email) => { if (!threads[email.threadId]) { threads[email.threadId] = { messages: [], participants: [], subject: email.subject, lastUpdated: email.date }; } const thread = threads[email.threadId]; thread.messages.push(email.id); if (!thread.participants.includes(email.from)) { thread.participants.push(email.from); } if (email.to && !thread.participants.includes(email.to)) { thread.participants.push(email.to); } const emailDate = new Date(email.date); const threadDate = new Date(thread.lastUpdated); if (emailDate > threadDate) { thread.lastUpdated = email.date; } return threads; }, {} as { [threadId: string]: ThreadInfo }); } /** * Get attachment metadata from message parts */ private getAttachmentMetadata(message: GmailMessage): IncomingGmailAttachment[] { const attachments: IncomingGmailAttachment[] = []; if (!message.payload?.parts) { return attachments; } for (const part of message.payload.parts) { if (part.filename && part.body?.attachmentId) { attachments.push({ id: part.body.attachmentId, name: part.filename, mimeType: part.mimeType || 'application/octet-stream', size: parseInt(String(part.body.size || '0')) }); } } return attachments; } /** * Enhanced getEmails method with support for advanced search criteria and options */ async getEmails({ email, search = {}, options = {}, messageIds }: GetEmailsParams): Promise<GetEmailsResponse> { try { const maxResults = options.maxResults || 10; let messages; let nextPageToken: string | undefined; if (messageIds && messageIds.length > 0) { messages = { messages: messageIds.map(id => ({ id })) }; } else { // Build search query from criteria const query = this.searchService.buildSearchQuery(search); // List messages matching query const client = this.ensureClient(); const { data } = await client.users.messages.list({ userId: 'me', q: query, maxResults, pageToken: options.pageToken, }); messages = data; nextPageToken = data.nextPageToken || undefined; } if (!messages.messages || messages.messages.length === 0) { return { emails: [], resultSummary: { total: 0, returned: 0, hasMore: false, searchCriteria: search } }; } // Get full message details const emails = await Promise.all( messages.messages.map(async (message) => { const client = this.ensureClient(); const { data: email } = await client.users.messages.get({ userId: 'me', id: message.id!, format: options.format || 'full', }); const headers = (email.payload?.headers || []).map(h => ({ name: h.name || '', value: h.value || '' })); const subject = headers.find(h => h.name === 'Subject')?.value || ''; const from = headers.find(h => h.name === 'From')?.value || ''; const to = headers.find(h => h.name === 'To')?.value || ''; const date = headers.find(h => h.name === 'Date')?.value || ''; // Get email body let body = ''; if (email.payload?.body?.data) { body = Buffer.from(email.payload.body.data, 'base64').toString(); } else if (email.payload?.parts) { const textPart = email.payload.parts.find(part => part.mimeType === 'text/plain'); if (textPart?.body?.data) { body = Buffer.from(textPart.body.data, 'base64').toString(); } } // Get attachment metadata if present and store in index const hasAttachments = email.payload?.parts?.some(part => part.filename && part.filename.length > 0) || false; let attachments; if (hasAttachments) { attachments = this.getAttachmentMetadata(email); // Store each attachment's metadata in the index attachments.forEach(attachment => { this.attachmentService.addAttachment(email.id!, attachment); }); } const response: EmailResponse = { id: email.id!, threadId: email.threadId!, labelIds: email.labelIds || undefined, snippet: email.snippet || undefined, subject, from, to, date, body, headers: options.includeHeaders ? this.extractHeaders(headers) : undefined, isUnread: email.labelIds?.includes('UNREAD') || false, hasAttachment: hasAttachments, attachments }; return response; }) ); // Handle threaded view if requested const threads = options.threadedView ? this.groupEmailsByThread(emails) : undefined; // Sort emails if requested if (options.sortOrder) { emails.sort((a, b) => { const dateA = new Date(a.date).getTime(); const dateB = new Date(b.date).getTime(); return options.sortOrder === 'asc' ? dateA - dateB : dateB - dateA; }); } // Transform response to simplify attachments const transformedResponse = this.responseTransformer.transformResponse({ emails, nextPageToken, resultSummary: { total: messages.resultSizeEstimate || emails.length, returned: emails.length, hasMore: Boolean(nextPageToken), searchCriteria: search }, threads }); return transformedResponse; } catch (error) { if (error instanceof GmailError) { throw error; } throw new GmailError( 'Failed to get emails', 'FETCH_ERROR', `Error: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } async sendEmail({ email, to, subject, body, cc = [], bcc = [], attachments = [] }: SendEmailParams): Promise<SendEmailResponse> { try { // Validate and prepare attachments for sending const processedAttachments = attachments?.map(attachment => { this.attachmentService.validateAttachment(attachment); const prepared = this.attachmentService.prepareAttachment(attachment); return { id: attachment.id, name: prepared.filename, mimeType: prepared.mimeType, size: attachment.size, content: prepared.content } as OutgoingGmailAttachment; }) || []; // Construct email with attachments const boundary = `boundary_${Date.now()}`; const messageParts = [ 'MIME-Version: 1.0\n', `Content-Type: multipart/mixed; boundary="${boundary}"\n`, `To: ${to.join(', ')}\n`, cc.length > 0 ? `Cc: ${cc.join(', ')}\n` : '', bcc.length > 0 ? `Bcc: ${bcc.join(', ')}\n` : '', `Subject: ${subject}\n\n`, `--${boundary}\n`, 'Content-Type: text/plain; charset="UTF-8"\n', 'Content-Transfer-Encoding: 7bit\n\n', body, '\n' ]; // Add attachments directly from content for (const attachment of processedAttachments) { messageParts.push( `--${boundary}\n`, `Content-Type: ${attachment.mimeType}\n`, 'Content-Transfer-Encoding: base64\n', `Content-Disposition: attachment; filename="${attachment.name}"\n\n`, attachment.content, '\n' ); } messageParts.push(`--${boundary}--`); const fullMessage = messageParts.join(''); // Encode the email in base64 const encodedMessage = Buffer.from(fullMessage) .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); // Send the email const client = this.ensureClient(); const { data } = await client.users.messages.send({ userId: 'me', requestBody: { raw: encodedMessage, }, }); const response: SendEmailResponse = { messageId: data.id!, threadId: data.threadId!, labelIds: data.labelIds || undefined }; if (processedAttachments.length > 0) { response.attachments = processedAttachments; } return response; } catch (error) { if (error instanceof GmailError) { throw error; } throw new GmailError( 'Failed to send email', 'SEND_ERROR', `Error: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } }