gmail.service.ts•9.18 kB
import { google } from "googleapis";
import type { OAuth2Client } from "google-auth-library";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import type {
  ListEmailsArgs,
  ReadEmailArgs,
  SendEmailArgs,
  EmailData,
  GmailMessageFormat,
  GmailMessageItem,
  GmailHeader,
  GmailMessagePart,
} from "../types/gmail.types.js";
export class GmailService {
  private gmail: ReturnType<typeof google.gmail> | null = null;
  private oauth2Client: OAuth2Client | null = null;
  constructor() {
    this.initializeGmail();
  }
  private initializeGmail(): void {
    const clientId = process.env.GMAIL_CLIENT_ID;
    const clientSecret = process.env.GMAIL_CLIENT_SECRET;
    const redirectUri = process.env.GMAIL_REDIRECT_URI || "urn:ietf:wg:oauth:2.0:oob";
    const refreshToken = process.env.GMAIL_REFRESH_TOKEN;
    if (!clientId || !clientSecret) {
      console.warn("[Gmail] Client ID or Secret not configured");
      return;
    }
    this.oauth2Client = new google.auth.OAuth2(clientId, clientSecret, redirectUri);
    if (refreshToken) {
      this.oauth2Client.setCredentials({ refresh_token: refreshToken });
      this.gmail = google.gmail({ version: "v1", auth: this.oauth2Client });
    } else {
      console.warn("[Gmail] Refresh token not configured");
    }
  }
  async getTools(): Promise<Tool[]> {
    if (!this.gmail) {
      return [];
    }
    return [
      {
        name: "gmail_list_emails",
        description: "List emails from Gmail inbox. Can filter by query, maxResults, and labelIds.",
        inputSchema: {
          type: "object",
          properties: {
            query: {
              type: "string",
              description: 'Search query string (e.g., "from:example@gmail.com subject:test")',
            },
            maxResults: {
              type: "number",
              description: "Maximum number of emails to return (default: 10)",
              default: 10,
            },
            labelIds: {
              type: "array",
              items: { type: "string" },
              description: 'Array of label IDs to filter by (e.g., ["INBOX", "UNREAD"])',
            },
          },
        },
      },
      {
        name: "gmail_read_email",
        description: "Read a specific email by its message ID",
        inputSchema: {
          type: "object",
          properties: {
            messageId: {
              type: "string",
              description: "The ID of the email message to read",
            },
            format: {
              type: "string",
              enum: ["full", "metadata", "minimal", "raw"],
              description: "Format of the email response (default: full)",
              default: "full",
            },
          },
          required: ["messageId"],
        },
      },
      {
        name: "gmail_send_email",
        description: "Send an email via Gmail",
        inputSchema: {
          type: "object",
          properties: {
            to: {
              type: "string",
              description: "Recipient email address",
            },
            subject: {
              type: "string",
              description: "Email subject",
            },
            body: {
              type: "string",
              description: "Email body (plain text)",
            },
            htmlBody: {
              type: "string",
              description: "Email body (HTML, optional)",
            },
            cc: {
              type: "string",
              description: "CC email address (optional)",
            },
            bcc: {
              type: "string",
              description: "BCC email address (optional)",
            },
          },
          required: ["to", "subject", "body"],
        },
      },
      {
        name: "gmail_get_labels",
        description: "Get list of all Gmail labels",
        inputSchema: {
          type: "object",
          properties: {},
        },
      },
    ];
  }
  async handleTool(name: string, args: Record<string, unknown>): Promise<{ content: Array<{ type: string; text: string }> }> {
    if (!this.gmail) {
      throw new Error("Gmail service not initialized. Please check your Gmail credentials.");
    }
    try {
      switch (name) {
        case "gmail_list_emails":
          return await this.listEmails(args as unknown as ListEmailsArgs);
        case "gmail_read_email":
          return await this.readEmail(args as unknown as ReadEmailArgs);
        case "gmail_send_email":
          return await this.sendEmail(args as unknown as SendEmailArgs);
        case "gmail_get_labels":
          return await this.getLabels();
        default:
          throw new Error(`Unknown Gmail tool: ${name}`);
      }
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : "Unknown error";
      throw new Error(`Gmail operation failed: ${errorMessage}`);
    }
  }
  private async listEmails(args: ListEmailsArgs): Promise<{ content: Array<{ type: string; text: string }> }> {
    const { query, maxResults = 10, labelIds } = args;
    const response = await this.gmail!.users.messages.list({
      userId: "me",
      q: query,
      maxResults,
      labelIds,
    });
    const messages = response.data.messages || [];
    const emailList = messages.map((msg: GmailMessageItem) => ({
      id: msg.id || null,
      threadId: msg.threadId || null,
    }));
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(emailList, null, 2),
        },
      ],
    };
  }
  private async readEmail(args: ReadEmailArgs): Promise<{ content: Array<{ type: string; text: string }> }> {
    const { messageId, format = "full" } = args;
    const response = await this.gmail!.users.messages.get({
      userId: "me",
      id: messageId,
      format: format as GmailMessageFormat,
    });
    const message = response.data;
    const emailData: EmailData = {
      id: message.id || null,
      threadId: message.threadId || null,
      labelIds: message.labelIds || null,
      snippet: message.snippet || null,
    };
    // Parse headers
    if (message.payload?.headers) {
      const headers: Record<string, string> = {};
      message.payload.headers.forEach((header: GmailHeader) => {
        if (header.name && header.value) {
          headers[header.name.toLowerCase()] = header.value;
        }
      });
      emailData.headers = headers;
    }
    // Extract body
    if (message.payload?.body?.data) {
      emailData.body = Buffer.from(message.payload.body.data, "base64").toString("utf-8");
    } else if (message.payload?.parts) {
      const bodyParts: string[] = [];
      message.payload.parts.forEach((part: GmailMessagePart) => {
        if (part.body?.data) {
          bodyParts.push(Buffer.from(part.body.data, "base64").toString("utf-8"));
        }
      });
      emailData.body = bodyParts.join("\n---\n");
    }
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(emailData, null, 2),
        },
      ],
    };
  }
  private async sendEmail(args: SendEmailArgs): Promise<{ content: Array<{ type: string; text: string }> }> {
    const { to, subject, body, htmlBody, cc, bcc } = args;
    const toLine = `To: ${to}`;
    const ccLine = cc ? `Cc: ${cc}` : "";
    const bccLine = bcc ? `Bcc: ${bcc}` : "";
    const subjectLine = `Subject: ${subject}`;
    const contentType = htmlBody ? "text/html" : "text/plain";
    const emailBody = htmlBody || body;
    const rawMessage = [toLine, ccLine, bccLine, subjectLine, `Content-Type: ${contentType}; charset=utf-8`, "", emailBody]
      .filter((line) => line !== "")
      .join("\r\n");
    const encodedMessage = Buffer.from(rawMessage).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
    const response = await this.gmail!.users.messages.send({
      userId: "me",
      requestBody: {
        raw: encodedMessage,
      },
    });
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(
            {
              success: true,
              messageId: response.data.id,
              threadId: response.data.threadId,
            },
            null,
            2
          ),
        },
      ],
    };
  }
  private async getLabels(): Promise<{ content: Array<{ type: string; text: string }> }> {
    const response = await this.gmail!.users.labels.list({
      userId: "me",
    });
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(response.data.labels || [], null, 2),
        },
      ],
    };
  }
  async readResource(uri: string): Promise<{ contents: Array<{ uri: string; mimeType: string; text: string }> }> {
    if (uri === "gmail://inbox") {
      const emails = await this.listEmails({ maxResults: 20 });
      return {
        contents: [
          {
            uri,
            mimeType: "application/json",
            text: emails.content[0].text,
          },
        ],
      };
    }
    throw new Error(`Unknown Gmail resource: ${uri}`);
  }
  async cleanup(): Promise<void> {
    // Cleanup if needed
  }
}