gmail.ts•29.7 kB
import { Tool, TextContent, ImageContent, EmbeddedResource } from '@modelcontextprotocol/sdk/types.js';
import { GAuthService } from '../services/gauth.js';
import { google } from 'googleapis';
import { USER_ID_ARG } from '../types/tool-handler.js';
import { Buffer } from 'buffer';
import fs from 'fs';
function decodeBase64Data(fileData: string): Buffer {
  const standardBase64Data = fileData.replace(/-/g, '+').replace(/_/g, '/');
  const padding = '='.repeat((4 - standardBase64Data.length % 4) % 4);
  return Buffer.from(standardBase64Data + padding, 'base64');
}
export class GmailTools {
  private gmail: ReturnType<typeof google.gmail>;
  constructor(private gauth: GAuthService) {
    this.gmail = google.gmail({ version: 'v1', auth: this.gauth.getClient() });
  }
  // Helper methods for email content extraction
  private decodeBase64UrlString(base64UrlString: string): string {
    try {
      const base64String = base64UrlString.replace(/-/g, '+').replace(/_/g, '/');
      const padding = '='.repeat((4 - base64String.length % 4) % 4);
      const base64 = base64String + padding;
      return Buffer.from(base64, 'base64').toString('utf-8');
    } catch (error) {
      console.error('Error decoding base64 string:', error);
      return '[Error decoding content]';
    }
  }
  private extractEmailText(payload: any): string {
    // For simple text emails
    if (payload.mimeType === 'text/plain' && payload.body?.data) {
      return this.decodeBase64UrlString(payload.body.data);
    }
    // For HTML-only emails, we'll still return the HTML content
    if (payload.mimeType === 'text/html' && payload.body?.data) {
      return this.decodeBase64UrlString(payload.body.data);
    }
    // For multipart emails, look for text/plain part first, then text/html
    if (payload.parts && Array.isArray(payload.parts)) {
      // First try to find text/plain part
      const textPart = payload.parts.find((part: any) => part.mimeType === 'text/plain');
      if (textPart && textPart.body?.data) {
        return this.decodeBase64UrlString(textPart.body.data);
      }
      // If no text/plain, try text/html
      const htmlPart = payload.parts.find((part: any) => part.mimeType === 'text/html');
      if (htmlPart && htmlPart.body?.data) {
        return this.decodeBase64UrlString(htmlPart.body.data);
      }
      // Recursively check nested multipart structures
      for (const part of payload.parts) {
        if (part.parts) {
          const nestedText = this.extractEmailText(part);
          if (nestedText) {
            return nestedText;
          }
        }
      }
    }
    return '';
  }
  private extractEmailHeaders(headers: any[]): Record<string, string> {
    const result: Record<string, string> = {};
    const importantHeaders = ['from', 'to', 'cc', 'bcc', 'subject', 'date', 'reply-to'];
    
    if (headers && Array.isArray(headers)) {
      headers.forEach(header => {
        if (header.name && header.value) {
          const headerName = header.name.toLowerCase();
          if (importantHeaders.includes(headerName)) {
            result[headerName] = header.value;
          }
        }
      });
    }
    return result;
  }
  getTools(): Tool[] {
    return ([
      {
        name: 'gmail_list_accounts',
        description: 'Lists all configured Google accounts that can be used with the Gmail tools. This tool does not require a user_id as it lists available accounts before selection.',
        inputSchema: {
          type: 'object',
          properties: {},
          additionalProperties: false,
          required: []
        }
      } as Tool,
      {
        name: 'gmail_query_emails',
        description: `Query Gmail emails based on an optional search query. 
        Returns emails in reverse chronological order (newest first).
        Returns metadata such as subject and also a short summary of the content.`,
        inputSchema: {
          type: 'object',
          properties: {
            [USER_ID_ARG]: {
              type: 'string',
              description: 'Email address of the user'
            },
            query: {
              type: 'string',
              description: `Gmail search query (optional). Examples:
                - a $string: Search email body, subject, and sender information for $string
                - 'is:unread' for unread emails
                - 'from:example@gmail.com' for emails from a specific sender
                - 'newer_than:2d' for emails from last 2 days
                - 'has:attachment' for emails with attachments
                If not provided, returns recent emails without filtering.`
            },
            max_results: {
              type: 'integer',
              description: 'Maximum number of emails to retrieve (1-500)',
              minimum: 1,
              maximum: 500,
              default: 100
            }
          },
          required: [USER_ID_ARG]
        }
      },
      {
        name: 'gmail_get_email',
        description: 'Retrieves a complete Gmail email message by its ID, including the full message body and attachment IDs.',
        inputSchema: {
          type: 'object',
          properties: {
            [USER_ID_ARG]: {
              type: 'string',
              description: 'Email address of the user'
            },
            email_id: {
              type: 'string',
              description: 'The ID of the Gmail message to retrieve'
            }
          },
          required: ['email_id', USER_ID_ARG]
        }
      },
      {
        name: 'gmail_bulk_get_emails',
        description: 'Retrieves multiple Gmail email messages by their IDs in a single request, including the full message bodies and attachment IDs.',
        inputSchema: {
          type: 'object',
          properties: {
            [USER_ID_ARG]: {
              type: 'string',
              description: 'Email address of the user'
            },
            email_ids: {
              type: 'array',
              items: {
                type: 'string'
              },
              description: 'List of Gmail message IDs to retrieve'
            }
          },
          required: ['email_ids', USER_ID_ARG]
        }
      },
      {
        name: 'gmail_create_draft',
        description: `Creates a draft email message from scratch in Gmail with specified recipient, subject, body, and optional CC recipients.
        
        Do NOT use this tool when you want to draft or send a REPLY to an existing message. This tool does NOT include any previous message content. Use the reply_gmail_email tool
        with send=false instead.`,
        inputSchema: {
          type: 'object',
          properties: {
            [USER_ID_ARG]: {
              type: 'string',
              description: 'Email address of the user'
            },
            to: {
              type: 'string',
              description: 'Email address of the recipient'
            },
            subject: {
              type: 'string',
              description: 'Subject line of the email'
            },
            body: {
              type: 'string',
              description: 'Body content of the email'
            },
            cc: {
              type: 'array',
              items: {
                type: 'string'
              },
              description: 'Optional list of email addresses to CC'
            }
          },
          required: ['to', 'subject', 'body', USER_ID_ARG]
        }
      },
      {
        name: 'gmail_delete_draft',
        description: 'Deletes a Gmail draft message by its ID. This action cannot be undone.',
        inputSchema: {
          type: 'object',
          properties: {
            [USER_ID_ARG]: {
              type: 'string',
              description: 'Email address of the user'
            },
            draft_id: {
              type: 'string',
              description: 'The ID of the draft to delete'
            }
          },
          required: ['draft_id', USER_ID_ARG]
        }
      },
      {
        name: 'gmail_reply',
        description: `Creates a reply to an existing Gmail email message and either sends it or saves as draft.
        Use this tool if you want to draft a reply. Use the 'cc' argument if you want to perform a "reply all".`,
        inputSchema: {
          type: 'object',
          properties: {
            [USER_ID_ARG]: {
              type: 'string',
              description: 'Email address of the user'
            },
            original_message_id: {
              type: 'string',
              description: 'The ID of the Gmail message to reply to'
            },
            reply_body: {
              type: 'string',
              description: 'The body content of your reply message'
            },
            send: {
              type: 'boolean',
              description: 'If true, sends the reply immediately. If false, saves as draft.',
              default: false
            },
            cc: {
              type: 'array',
              items: {
                type: 'string'
              },
              description: 'Optional list of email addresses to CC on the reply'
            }
          },
          required: ['original_message_id', 'reply_body', USER_ID_ARG]
        }
      },
      {
        name: 'gmail_get_attachment',
        description: 'Retrieves a Gmail attachment by its ID.',
        inputSchema: {
          type: 'object',
          properties: {
            [USER_ID_ARG]: {
              type: 'string',
              description: 'Email address of the user'
            },
            message_id: {
              type: 'string',
              description: 'The ID of the Gmail message containing the attachment'
            },
            attachment_id: {
              type: 'string',
              description: 'The ID of the attachment to retrieve'
            },
            mime_type: {
              type: 'string',
              description: 'The MIME type of the attachment'
            },
            filename: {
              type: 'string',
              description: 'The filename of the attachment'
            },
            save_to_disk: {
              type: 'string',
              description: 'The fullpath to save the attachment to disk. If not provided, the attachment is returned as a resource.'
            }
          },
          required: ['message_id', 'attachment_id', 'mime_type', 'filename', USER_ID_ARG]
        }
      },
      {
        name: 'gmail_bulk_save_attachments',
        description: 'Saves multiple Gmail attachments to disk by their message IDs and attachment IDs in a single request.',
        inputSchema: {
          type: 'object',
          properties: {
            [USER_ID_ARG]: {
              type: 'string',
              description: 'Email address of the user'
            },
            attachments: {
              type: 'array',
              items: {
                type: 'object',
                properties: {
                  message_id: {
                    type: 'string',
                    description: 'ID of the Gmail message containing the attachment'
                  },
                  part_id: {
                    type: 'string',
                    description: 'ID of the part containing the attachment'
                  },
                  save_path: {
                    type: 'string',
                    description: 'Path where the attachment should be saved'
                  }
                },
                required: ['message_id', 'part_id', 'save_path']
              }
            }
          },
          required: ['attachments', USER_ID_ARG]
        }
      },
      {
        name: 'gmail_archive',
        description: 'Archives a Gmail message by removing it from the inbox.',
        inputSchema: {
          type: 'object',
          properties: {
            [USER_ID_ARG]: {
              type: 'string',
              description: 'Email address of the user'
            },
            message_id: {
              type: 'string',
              description: 'The ID of the Gmail message to archive'
            }
          },
          required: ['message_id', USER_ID_ARG]
        }
      },
      {
        name: 'gmail_bulk_archive',
        description: 'Archives multiple Gmail messages by removing them from the inbox.',
        inputSchema: {
          type: 'object',
          properties: {
            [USER_ID_ARG]: {
              type: 'string',
              description: 'Email address of the user'
            },
            message_ids: {
              type: 'array',
              items: {
                type: 'string'
              },
              description: 'List of Gmail message IDs to archive'
            }
          },
          required: ['message_ids', USER_ID_ARG]
        }
      }
    ] as Tool[]).filter(tool => (
      (process.env.GMAIL_ALLOW_SENDING === 'true')
      ? true
      : (tool.name !== 'gmail_reply' && tool.name !== 'gmail_create_draft')));
  }
  async handleTool(name: string, args: Record<string, any>): Promise<Array<TextContent | ImageContent | EmbeddedResource>> {
    switch (name) {
      case 'gmail_list_accounts':
        return this.listAccounts();
      case 'gmail_query_emails':
        return this.queryEmails(args);
      case 'gmail_get_email':
        return this.getEmailById(args);
      case 'gmail_bulk_get_emails':
        return this.bulkGetEmails(args);
      case 'gmail_create_draft':
        return this.createDraft(args);
      case 'gmail_delete_draft':
        return this.deleteDraft(args);
      case 'gmail_reply':
        return this.reply(args);
      case 'gmail_get_attachment':
        return this.getAttachment(args);
      case 'gmail_bulk_save_attachments':
        return this.bulkSaveAttachments(args);
      case 'gmail_archive':
        return this.archive(args);
      case 'gmail_bulk_archive':
        return this.bulkArchive(args);
      // Add other tool handlers here...
      default:
        throw new Error(`Unknown tool: ${name}`);
    }
  }
  private async listAccounts(): Promise<Array<TextContent>> {
    try {
      const accounts = await this.gauth.getAccountInfo();
      const accountList = accounts.map(account => ({
        email: account.email,
        accountType: account.accountType,
        extraInfo: account.extraInfo,
        description: account.toDescription()
      }));
      if (accountList.length === 0) {
        return [{
          type: 'text',
          text: JSON.stringify({
            message: 'No accounts configured. Please check your .accounts.json file.',
            accounts: []
          }, null, 2)
        }];
      }
      return [{
        type: 'text',
        text: JSON.stringify({
          message: `Found ${accountList.length} configured account(s)`,
          accounts: accountList
        }, null, 2)
      }];
    } catch (error) {
      console.error('Error listing accounts:', error);
      return [{
        type: 'text',
        text: JSON.stringify({
          error: `Failed to list accounts: ${(error as Error).message}`,
          accounts: []
        }, null, 2)
      }];
    }
  }
  private async queryEmails(args: Record<string, any>): Promise<Array<TextContent>> {
    const userId = args[USER_ID_ARG];
    if (!userId) {
      throw new Error(`Missing required argument: ${USER_ID_ARG}`);
    }
    try {
      const response = await this.gmail.users.messages.list({
        userId,
        q: args.query,
        maxResults: args.max_results || 100
      });
      const messages = response.data.messages || [];
      const emails = await Promise.all(
        messages.map(async (msg) => {
          const email = await this.gmail.users.messages.get({
            userId,
            id: msg.id!,
            format: 'metadata',
            metadataHeaders: ['From', 'To', 'Subject', 'Date']
          });
          // Extract headers into a more readable format
          const headers: Record<string, string> = {};
          email.data.payload?.headers?.forEach(header => {
            if (header.name && header.value) {
              headers[header.name.toLowerCase()] = header.value;
            }
          });
          return {
            id: email.data.id,
            threadId: email.data.threadId,
            labelIds: email.data.labelIds,
            snippet: email.data.snippet,
            internalDate: email.data.internalDate,
            headers
          };
        })
      );
      return [{
        type: 'text',
        text: JSON.stringify(emails, null, 2)
      }];
    } catch (error) {
      console.error('Error querying emails:', error);
      throw error;
    }
  }
  private async getEmailById(args: Record<string, any>): Promise<Array<TextContent>> {
    const userId = args[USER_ID_ARG];
    const emailId = args.email_id;
    if (!userId) {
      throw new Error(`Missing required argument: ${USER_ID_ARG}`);
    }
    if (!emailId) {
      throw new Error('Missing required argument: email_id');
    }
    try {
      const email = await this.gmail.users.messages.get({
        userId,
        id: emailId,
        format: 'full'
      });
      // Extract headers
      const headers = this.extractEmailHeaders(email.data.payload?.headers || []);
      
      // Extract text content
      const textContent = this.extractEmailText(email.data.payload || {});
      // Get attachments if any
      const attachments: Record<string, any> = {};
      if (email.data.payload?.parts) {
        for (const part of email.data.payload.parts) {
          if (part.body?.attachmentId) {
            attachments[part.partId!] = {
              filename: part.filename,
              mimeType: part.mimeType,
              attachmentId: part.body.attachmentId
            };
          }
        }
      }
      // Create simplified email object
      const result = {
        id: email.data.id,
        threadId: email.data.threadId,
        labelIds: email.data.labelIds,
        headers,
        textContent,
        hasAttachments: Object.keys(attachments).length > 0,
        attachments: Object.keys(attachments).length > 0 ? attachments : undefined
      };
      return [{
        type: 'text',
        text: JSON.stringify(result, null, 2)
      }];
    } catch (error) {
      console.error('Error getting email:', error);
      throw error;
    }
  }
  private async bulkGetEmails(args: Record<string, any>): Promise<Array<TextContent>> {
    const userId = args[USER_ID_ARG];
    const emailIds = args.email_ids;
    if (!userId) {
      throw new Error(`Missing required argument: ${USER_ID_ARG}`);
    }
    if (!emailIds || emailIds.length === 0) {
      throw new Error('Missing required argument: email_ids');
    }
    try {
      const emails = await Promise.all(
        emailIds.map(async (emailId: string) => {
          const email = await this.gmail.users.messages.get({
            userId,
            id: emailId,
            format: 'full'
          });
          // Extract headers
          const headers = this.extractEmailHeaders(email.data.payload?.headers || []);
          
          // Extract text content
          const textContent = this.extractEmailText(email.data.payload || {});
          // Get attachments if any
          const attachments: Record<string, any> = {};
          if (email.data.payload?.parts) {
            for (const part of email.data.payload.parts) {
              if (part.body?.attachmentId) {
                attachments[part.partId!] = {
                  filename: part.filename,
                  mimeType: part.mimeType,
                  attachmentId: part.body.attachmentId
                };
              }
            }
          }
          // Create simplified email object
          return {
            id: email.data.id,
            threadId: email.data.threadId,
            labelIds: email.data.labelIds,
            headers,
            textContent,
            hasAttachments: Object.keys(attachments).length > 0,
            attachments: Object.keys(attachments).length > 0 ? attachments : undefined
          };
        })
      );
      return [{
        type: 'text',
        text: JSON.stringify(emails, null, 2)
      }];
    } catch (error) {
      console.error('Error getting emails:', error);
      throw error;
    }
  }
  private async createDraft(args: Record<string, any>): Promise<Array<TextContent>> {
    const userId = args[USER_ID_ARG];
    const to = args.to;
    const subject = args.subject;
    const body = args.body;
    const cc = args.cc || [];
    if (!userId) {
      throw new Error(`Missing required argument: ${USER_ID_ARG}`);
    }
    if (!to) {
      throw new Error('Missing required argument: to');
    }
    if (!subject) {
      throw new Error('Missing required argument: subject');
    }
    if (!body) {
      throw new Error('Missing required argument: body');
    }
    try {
      const message = {
        raw: Buffer.from(
          `To: ${to}\r\n` +
          `Subject: ${subject}\r\n` +
          `Cc: ${cc.join(', ')}\r\n` +
          `Content-Type: text/plain; charset="UTF-8"\r\n` +
          `\r\n` +
          `${body}`
        ).toString('base64url')
      };
      const draft = await this.gmail.users.drafts.create({
        userId,
        requestBody: {
          message
        }
      });
      return [{
        type: 'text',
        text: JSON.stringify(draft.data, null, 2)
      }];
    } catch (error) {
      console.error('Error creating draft:', error);
      throw error;
    }
  }
  private async deleteDraft(args: Record<string, any>): Promise<Array<TextContent>> {
    const userId = args[USER_ID_ARG];
    const draftId = args.draft_id;
    if (!userId) {
      throw new Error(`Missing required argument: ${USER_ID_ARG}`);
    }
    if (!draftId) {
      throw new Error('Missing required argument: draft_id');
    }
    try {
      await this.gmail.users.drafts.delete({
        userId,
        id: draftId
      });
      return [{
        type: 'text',
        text: `Draft ${draftId} deleted successfully`
      }];
    } catch (error) {
      console.error('Error deleting draft:', error);
      throw error;
    }
  }
  private async reply(args: Record<string, any>): Promise<Array<TextContent>> {
    const userId = args[USER_ID_ARG];
    const originalMessageId = args.original_message_id;
    const replyBody = args.reply_body;
    // NEVER SEND EMAILS
    const send = false; // args.send || false;
    const cc = args.cc || [];
    if (!userId) {
      throw new Error(`Missing required argument: ${USER_ID_ARG}`);
    }
    if (!originalMessageId) {
      throw new Error('Missing required argument: original_message_id');
    }
    if (!replyBody) {
      throw new Error('Missing required argument: reply_body');
    }
    try {
      // First get the original message to extract headers
      const originalMessage = await this.gmail.users.messages.get({
        userId,
        id: originalMessageId
      });
      const headers = originalMessage.data.payload?.headers?.reduce((acc: Record<string, string>, header) => {
        if (header.name && header.value) {
          acc[header.name.toLowerCase()] = header.value;
        }
        return acc;
      }, {});
      if (!headers) {
        throw new Error('Could not extract headers from original message');
      }
      // Get the threadId from the original message
      const threadId = originalMessage.data.threadId;
      if (!threadId) {
        throw new Error('Could not extract threadId from original message');
      }
      const message = {
        raw: Buffer.from(
          `In-Reply-To: ${originalMessageId}\r\n` +
          `References: ${originalMessageId}\r\n` +
          `Subject: Re: ${headers.subject || ''}\r\n` +
          `To: ${headers.from || ''}\r\n` +
          `Cc: ${cc.join(', ')}\r\n` +
          `Content-Type: text/plain; charset="UTF-8"\r\n` +
          `\r\n` +
          `${replyBody}`
        ).toString('base64url'),
        threadId: threadId
      };
      if (send) {
        await this.gmail.users.messages.send({
          userId,
          requestBody: {
            raw: message.raw,
            threadId: message.threadId
          }
        });
        return [{
          type: 'text',
          text: 'Reply sent successfully'
        }];
      } else {
        const draft = await this.gmail.users.drafts.create({
          userId,
          requestBody: {
            message
          }
        });
        return [{
          type: 'text',
          text: JSON.stringify(draft.data, null, 2)
        }];
      }
    } catch (error) {
      console.error('Error replying to email:', error);
      throw error;
    }
  }
  private async getAttachment(args: Record<string, any>): Promise<Array<TextContent>> {
    const userId = args[USER_ID_ARG];
    const messageId = args.message_id;
    const attachmentId = args.attachment_id;
    const mimeType = args.mime_type;
    const filename = args.filename;
    const saveToDisk = args.save_to_disk;
    if (!userId) {
      throw new Error(`Missing required argument: ${USER_ID_ARG}`);
    }
    if (!messageId) {
      throw new Error('Missing required argument: message_id');
    }
    if (!attachmentId) {
      throw new Error('Missing required argument: attachment_id');
    }
    if (!mimeType) {
      throw new Error('Missing required argument: mime_type');
    }
    if (!filename) {
      throw new Error('Missing required argument: filename');
    }
    try {
      const attachment = await this.gmail.users.messages.attachments.get({
        userId,
        messageId,
        id: attachmentId
      });
      const attachmentData = attachment.data.data;
      if (!attachmentData) {
        throw new Error('Attachment data not found');
      }
      const decodedData = Buffer.from(attachmentData, 'base64').toString('utf-8');
      const decodedContent = this.decodeBase64UrlString(decodedData);
      if (saveToDisk) {
        fs.writeFileSync(saveToDisk, decodedContent);
        return [{
          type: 'text',
          text: `Attachment saved to ${saveToDisk}`
        }];
      } else {
        return [{
          type: 'text',
          text: decodedContent
        }];
      }
    } catch (error) {
      console.error('Error getting attachment:', error);
      throw error;
    }
  }
  private async bulkSaveAttachments(args: Record<string, any>): Promise<Array<TextContent>> {
    const userId = args[USER_ID_ARG];
    const attachments = args.attachments;
    if (!userId) {
      throw new Error(`Missing required argument: ${USER_ID_ARG}`);
    }
    if (!attachments || attachments.length === 0) {
      throw new Error('Missing required argument: attachments');
    }
    try {
      const results = await Promise.all(
        attachments.map(async (attachmentInfo: any) => {
          const messageId = attachmentInfo.message_id;
          const partId = attachmentInfo.part_id;
          const savePath = attachmentInfo.save_path;
          if (!messageId || !partId || !savePath) {
            throw new Error('Missing required arguments: message_id, part_id, or save_path');
          }
          const attachmentData = await this.gmail.users.messages.attachments.get({
            userId,
            messageId,
            id: partId
          });
          const fileData = attachmentData.data.data;
          if (!fileData) {
            throw new Error('Attachment data not found');
          }
          const decodedData = Buffer.from(fileData, 'base64').toString('utf-8');
          const decodedContent = this.decodeBase64UrlString(decodedData);
          fs.writeFileSync(savePath, decodedContent);
          return {
            messageId,
            partId,
            savePath,
            status: 'success'
          };
        })
      );
      return [{
        type: 'text',
        text: JSON.stringify(results, null, 2)
      }];
    } catch (error) {
      console.error('Error saving attachments:', error);
      throw error;
    }
  }
  private async archive(args: Record<string, any>): Promise<Array<TextContent>> {
    const userId = args[USER_ID_ARG];
    const messageId = args.message_id;
    if (!userId) {
      throw new Error(`Missing required argument: ${USER_ID_ARG}`);
    }
    if (!messageId) {
      throw new Error('Missing required argument: message_id');
    }
    try {
      await this.gmail.users.messages.trash({
        userId,
        id: messageId
      });
      return [{
        type: 'text',
        text: `Message ${messageId} archived successfully`
      }];
    } catch (error) {
      console.error('Error archiving message:', error);
      throw error;
    }
  }
  private async bulkArchive(args: Record<string, any>): Promise<Array<TextContent>> {
    const userId = args[USER_ID_ARG];
    const messageIds = args.message_ids;
    if (!userId) {
      throw new Error(`Missing required argument: ${USER_ID_ARG}`);
    }
    if (!messageIds || messageIds.length === 0) {
      throw new Error('Missing required argument: message_ids');
    }
    try {
      const results = await Promise.all(
        messageIds.map(async (messageId: string) => {
          await this.gmail.users.messages.trash({
            userId,
            id: messageId
          });
          return {
            messageId,
            status: 'archived'
          };
        })
      );
      return [{
        type: 'text',
        text: JSON.stringify(results, null, 2)
      }];
    } catch (error) {
      console.error('Error archiving messages:', error);
      throw error;
    }
  }
}