mcp-memory-libsql

by spences10
Verified
import { google } from 'googleapis'; import { GmailError, OutgoingGmailAttachment, IncomingGmailAttachment } from '../types.js'; import { GmailAttachmentService } from './attachment.js'; export type DraftAction = 'create' | 'read' | 'update' | 'delete' | 'send'; export interface ManageDraftParams { email: string; action: DraftAction; draftId?: string; data?: DraftData; } export interface DraftData { to: string[]; subject: string; body: string; cc?: string[]; bcc?: string[]; threadId?: string; // For reply drafts attachments?: OutgoingGmailAttachment[]; } export class DraftService { private gmailClient?: ReturnType<typeof google.gmail>; constructor(private attachmentService: GmailAttachmentService) {} async initialize(): Promise<void> { // Initialization will be handled by Gmail service } 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; } async createDraft(email: string, data: DraftData) { try { const client = this.ensureClient(); // Validate and prepare attachments const processedAttachments = data.attachments?.map(attachment => { this.attachmentService.validateAttachment(attachment); return this.attachmentService.prepareAttachment(attachment); }) || []; // Construct email with attachments const boundary = `boundary_${Date.now()}`; const messageParts = [ 'MIME-Version: 1.0\n', `Content-Type: multipart/mixed; boundary="${boundary}"\n`, `To: ${data.to.join(', ')}\n`, data.cc?.length ? `Cc: ${data.cc.join(', ')}\n` : '', data.bcc?.length ? `Bcc: ${data.bcc.join(', ')}\n` : '', `Subject: ${data.subject}\n\n`, `--${boundary}\n`, 'Content-Type: text/plain; charset="UTF-8"\n', 'Content-Transfer-Encoding: 7bit\n\n', data.body, '\n' ]; // Add attachments for (const attachment of processedAttachments) { messageParts.push( `--${boundary}\n`, `Content-Type: ${attachment.mimeType}\n`, 'Content-Transfer-Encoding: base64\n', `Content-Disposition: attachment; filename="${attachment.filename}"\n\n`, attachment.content.toString(), '\n' ); } messageParts.push(`--${boundary}--`); const fullMessage = messageParts.join(''); // Create draft with threadId if it's a reply const { data: draft } = await client.users.drafts.create({ userId: 'me', requestBody: { message: { raw: Buffer.from(fullMessage).toString('base64'), threadId: data.threadId // Include threadId for replies } } }); return { id: draft.id || '', message: { id: draft.message?.id || '', threadId: draft.message?.threadId || '', labelIds: draft.message?.labelIds || [] }, updated: new Date().toISOString(), attachments: data.attachments }; } catch (error) { throw new GmailError( 'Failed to create draft', 'CREATE_ERROR', error instanceof Error ? error.message : 'Unknown error' ); } } async listDrafts(email: string) { try { const client = this.ensureClient(); const { data } = await client.users.drafts.list({ userId: 'me' }); // Get full details for each draft const drafts = await Promise.all((data.drafts || []) .filter((draft): draft is { id: string } => typeof draft.id === 'string') .map(async draft => { try { return await this.getDraft(email, draft.id); } catch (error) { // Log error but continue with other drafts console.error(`Failed to get draft ${draft.id}:`, error); return null; } }) ); // Filter out any failed draft fetches const successfulDrafts = drafts.filter((draft): draft is NonNullable<typeof draft> => draft !== null); return { drafts: successfulDrafts, nextPageToken: data.nextPageToken || undefined, resultSizeEstimate: data.resultSizeEstimate || 0 }; } catch (error) { throw new GmailError( 'Failed to list drafts', 'LIST_ERROR', error instanceof Error ? error.message : 'Unknown error' ); } } async getDraft(email: string, draftId: string) { try { const client = this.ensureClient(); const { data } = await client.users.drafts.get({ userId: 'me', id: draftId, format: 'full' }); if (!data.id || !data.message?.id || !data.message?.threadId) { throw new GmailError( 'Invalid response from Gmail API', 'GET_ERROR', 'Message ID or Thread ID is missing' ); } return { id: data.id, message: { id: data.message.id, threadId: data.message.threadId, labelIds: data.message.labelIds || [] }, updated: new Date().toISOString() // Gmail API doesn't provide updated time, using current time }; } catch (error) { throw new GmailError( 'Failed to get draft', 'GET_ERROR', error instanceof Error ? error.message : 'Unknown error' ); } } async updateDraft(email: string, draftId: string, data: DraftData) { try { const client = this.ensureClient(); // Validate and prepare attachments const processedAttachments = data.attachments?.map(attachment => { this.attachmentService.validateAttachment(attachment); return this.attachmentService.prepareAttachment(attachment); }) || []; // Construct updated email const boundary = `boundary_${Date.now()}`; const messageParts = [ 'MIME-Version: 1.0\n', `Content-Type: multipart/mixed; boundary="${boundary}"\n`, `To: ${data.to.join(', ')}\n`, data.cc?.length ? `Cc: ${data.cc.join(', ')}\n` : '', data.bcc?.length ? `Bcc: ${data.bcc.join(', ')}\n` : '', `Subject: ${data.subject}\n\n`, `--${boundary}\n`, 'Content-Type: text/plain; charset="UTF-8"\n', 'Content-Transfer-Encoding: 7bit\n\n', data.body, '\n' ]; // Add attachments for (const attachment of processedAttachments) { messageParts.push( `--${boundary}\n`, `Content-Type: ${attachment.mimeType}\n`, 'Content-Transfer-Encoding: base64\n', `Content-Disposition: attachment; filename="${attachment.filename}"\n\n`, attachment.content, '\n' ); } messageParts.push(`--${boundary}--`); const fullMessage = messageParts.join(''); // Update draft const { data: draft } = await client.users.drafts.update({ userId: 'me', id: draftId, requestBody: { message: { raw: Buffer.from(fullMessage).toString('base64') } } }); return { id: draft.id || '', message: { id: draft.message?.id || '', threadId: draft.message?.threadId || '', labelIds: draft.message?.labelIds || [] }, updated: new Date().toISOString(), attachments: data.attachments }; } catch (error) { throw new GmailError( 'Failed to update draft', 'UPDATE_ERROR', error instanceof Error ? error.message : 'Unknown error' ); } } async deleteDraft(email: string, draftId: string) { try { const client = this.ensureClient(); await client.users.drafts.delete({ userId: 'me', id: draftId }); return; } catch (error) { throw new GmailError( 'Failed to delete draft', 'DELETE_ERROR', error instanceof Error ? error.message : 'Unknown error' ); } } async manageDraft(params: ManageDraftParams) { const { email, action, draftId, data } = params; switch (action) { case 'create': if (!data) { throw new GmailError( 'Draft data is required for create action', 'INVALID_PARAMS' ); } return this.createDraft(email, data); case 'read': if (!draftId) { return this.listDrafts(email); } return this.getDraft(email, draftId); case 'update': if (!draftId || !data) { throw new GmailError( 'Draft ID and data are required for update action', 'INVALID_PARAMS' ); } return this.updateDraft(email, draftId, data); case 'delete': if (!draftId) { throw new GmailError( 'Draft ID is required for delete action', 'INVALID_PARAMS' ); } return this.deleteDraft(email, draftId); case 'send': if (!draftId) { throw new GmailError( 'Draft ID is required for send action', 'INVALID_PARAMS' ); } return this.sendDraft(email, draftId); default: throw new GmailError( 'Invalid action', 'INVALID_PARAMS', 'Supported actions are: create, read, update, delete, send' ); } } async sendDraft(email: string, draftId: string) { try { const client = this.ensureClient(); const { data } = await client.users.drafts.send({ userId: 'me', requestBody: { id: draftId } }); if (!data.id || !data.threadId) { throw new GmailError( 'Invalid response from Gmail API', 'SEND_ERROR', 'Message ID or Thread ID is missing' ); } return { messageId: data.id, threadId: data.threadId, labelIds: data.labelIds || undefined }; } catch (error) { throw new GmailError( 'Failed to send draft', 'SEND_ERROR', error instanceof Error ? error.message : 'Unknown error' ); } } }