#
tokens: 10341/50000 2/76 files (page 2/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 2 of 2. Use http://codebase.md/ejb503/systemprompt-mcp-gmail?lines=false&page={x} to view the full context.

# Directory Structure

```
├── .babelrc
├── .env.example
├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── CHANGELOG.md
├── config
│   ├── __llm__
│   │   └── README.md
│   └── server-config.ts
├── eslint.config.js
├── jest.config.mjs
├── jest.setup.ts
├── LICENSE.md
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── __mocks__
│   │   ├── @modelcontextprotocol
│   │   │   └── sdk.ts
│   │   ├── node_process.ts
│   │   ├── server.ts
│   │   └── systemprompt-service.ts
│   ├── __tests__
│   │   ├── index.test.ts
│   │   ├── mock-objects.ts
│   │   ├── server.test.ts
│   │   ├── test-utils.test.ts
│   │   └── test-utils.ts
│   ├── config
│   │   ├── __llm__
│   │   │   └── README.md
│   │   ├── __tests__
│   │   │   └── server-config.test.ts
│   │   └── server-config.ts
│   ├── constants
│   │   ├── instructions.ts
│   │   ├── message-handler.ts
│   │   ├── sampling-prompts.ts
│   │   └── tools.ts
│   ├── handlers
│   │   ├── __llm__
│   │   │   └── README.md
│   │   ├── __tests__
│   │   │   ├── callbacks.test.ts
│   │   │   ├── notifications.test.ts
│   │   │   ├── prompt-handlers.test.ts
│   │   │   ├── resource-handlers.test.ts
│   │   │   ├── sampling.test.ts
│   │   │   └── tool-handlers.test.ts
│   │   ├── callbacks.ts
│   │   ├── notifications.ts
│   │   ├── prompt-handlers.ts
│   │   ├── resource-handlers.ts
│   │   ├── sampling.ts
│   │   └── tool-handlers.ts
│   ├── index.ts
│   ├── schemas
│   │   └── generated
│   │       ├── index.ts
│   │       ├── SystempromptAgentRequestSchema.ts
│   │       ├── SystempromptBlockRequestSchema.ts
│   │       └── SystempromptPromptRequestSchema.ts
│   ├── server.ts
│   ├── services
│   │   ├── __llm__
│   │   │   └── README.md
│   │   ├── __tests__
│   │   │   ├── gmail-service.test.ts
│   │   │   ├── google-auth-service.test.ts
│   │   │   ├── google-base-service.test.ts
│   │   │   └── systemprompt-service.test.ts
│   │   ├── gmail-service.ts
│   │   ├── google-auth-service.ts
│   │   ├── google-base-service.ts
│   │   └── systemprompt-service.ts
│   ├── types
│   │   ├── __llm__
│   │   │   └── README.md
│   │   ├── gmail-types.ts
│   │   ├── index.ts
│   │   ├── sampling-schemas.ts
│   │   ├── sampling.ts
│   │   ├── systemprompt.ts
│   │   ├── tool-args.ts
│   │   └── tool-schemas.ts
│   └── utils
│       ├── __tests__
│       │   ├── mcp-mappers.test.ts
│       │   ├── message-handlers.test.ts
│       │   ├── tool-validation.test.ts
│       │   └── validation.test.ts
│       ├── mcp-mappers.ts
│       ├── message-handlers.ts
│       ├── tool-validation.ts
│       └── validation.ts
├── tsconfig.json
└── tsconfig.test.json
```

# Files

--------------------------------------------------------------------------------
/src/services/gmail-service.ts:
--------------------------------------------------------------------------------

```typescript
import { google } from "googleapis";
import { GoogleBaseService } from "./google-base-service.js";
import { gmail_v1 } from "googleapis/build/src/apis/gmail/v1.js";
import { EmailMetadata, SendEmailOptions, DraftEmailOptions } from "../types/gmail-types.js";

const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

function validateEmail(email: string): boolean {
  return EMAIL_REGEX.test(email.trim());
}

function validateEmailList(emails: string | string[] | undefined): void {
  if (!emails) return;

  const emailList = Array.isArray(emails) ? emails : emails.split(",").map((e) => e.trim());

  for (const email of emailList) {
    if (!validateEmail(email)) {
      throw new Error(`Invalid email address: ${email}`);
    }
  }
}

export class GmailService extends GoogleBaseService {
  private gmail!: gmail_v1.Gmail;
  private labelCache: Map<string, gmail_v1.Schema$Label> = new Map();
  private gmailInitPromise: Promise<void>;

  constructor() {
    super();
    this.gmailInitPromise = this.initializeGmailClient();
  }

  private async initializeGmailClient(): Promise<void> {
    await this.waitForInit();
    this.gmail = google.gmail({ version: "v1", auth: this.auth.getAuth() });
  }

  // Helper method to ensure initialization is complete
  private async ensureInitialized(): Promise<void> {
    await this.gmailInitPromise;
  }

  private async loadLabels(): Promise<void> {
    await this.ensureInitialized();
    if (this.labelCache.size === 0) {
      const labels = await this.getLabels();
      labels.forEach((label) => {
        this.labelCache.set(label.id!, label);
      });
    }
  }

  private parseEmailAddress(address: string): { name?: string; email: string } {
    const match = address.match(/(?:"?([^"]*)"?\s)?(?:<)?(.+@[^>]+)(?:>)?/);
    if (match) {
      return {
        name: match[1]?.trim(),
        email: match[2].trim(),
      };
    }
    return { email: address.trim() };
  }

  private async getMessageMetadata(messageId: string): Promise<gmail_v1.Schema$Message> {
    await this.ensureInitialized();
    try {
      const response = await this.gmail.users.messages.get({
        userId: "me",
        id: messageId,
        format: "metadata",
        metadataHeaders: [
          "From",
          "To",
          "Cc",
          "Bcc",
          "Subject",
          "Date",
          "Reply-To",
          "Message-ID",
          "References",
          "Content-Type",
        ],
      });
      return response.data;
    } catch (error) {
      console.error(`Failed to get message metadata for ${messageId}:`, error);
      throw error;
    }
  }

  private async extractEmailMetadata(message: gmail_v1.Schema$Message): Promise<EmailMetadata> {
    await this.loadLabels();
    const headers = message.payload?.headers || [];
    const fromHeader = headers.find((h) => h.name === "From")?.value || "";
    const toHeader = headers.find((h) => h.name === "To")?.value || "";
    const dateStr = headers.find((h) => h.name === "Date")?.value;

    const labels = (message.labelIds || [])
      .map((id) => {
        const label = this.labelCache.get(id);
        return label ? { id, name: label.name || id } : null;
      })
      .filter((label): label is { id: string; name: string } => label !== null);

    return {
      id: message.id!,
      threadId: message.threadId!,
      snippet: message.snippet?.replace(/&#39;/g, "'").replace(/&quot;/g, '"') || "",
      from: this.parseEmailAddress(fromHeader),
      to: toHeader.split(",").map((addr) => this.parseEmailAddress(addr.trim())),
      subject: headers.find((h) => h.name === "Subject")?.value || "(no subject)",
      date: dateStr ? new Date(dateStr) : new Date(),
      labels,
      hasAttachments: Boolean(
        message.payload?.parts?.some((part) => part.filename && part.filename.length > 0),
      ),
      isUnread: message.labelIds?.includes("UNREAD") || false,
      isImportant: message.labelIds?.includes("IMPORTANT") || false,
    };
  }

  async listMessages(maxResults: number = 100): Promise<EmailMetadata[]> {
    await this.ensureInitialized();
    try {
      const response = await this.gmail.users.messages.list({
        userId: "me",
        maxResults,
      });

      const messages = response.data.messages || [];
      const messageDetails = await Promise.all(
        messages.map((msg) => this.getMessageMetadata(msg.id!)),
      );

      return await Promise.all(messageDetails.map((msg) => this.extractEmailMetadata(msg)));
    } catch (error) {
      console.error("Failed to list Gmail messages:", error);
      throw error;
    }
  }

  async getMessage(messageId: string): Promise<EmailMetadata & { body: string }> {
    await this.ensureInitialized();
    try {
      const response = await this.gmail.users.messages.get({
        userId: "me",
        id: messageId,
        format: "full",
      });

      const metadata = await this.extractEmailMetadata(response.data);
      let body = "";

      // Extract message body
      const message = response.data;
      if (message.payload) {
        if (message.payload.body?.data) {
          body = Buffer.from(message.payload.body.data, "base64").toString("utf8");
        } else if (message.payload.parts) {
          const textPart = message.payload.parts.find(
            (part) => part.mimeType === "text/plain" || part.mimeType === "text/html",
          );
          if (textPart?.body?.data) {
            body = Buffer.from(textPart.body.data, "base64").toString("utf8");
          }
        }
      }

      return {
        ...metadata,
        body,
      };
    } catch (error) {
      console.error("Failed to get Gmail message:", error);
      throw error;
    }
  }

  async searchMessages(query: string, maxResults: number = 10): Promise<EmailMetadata[]> {
    await this.ensureInitialized();
    try {
      const response = await this.gmail.users.messages.list({
        userId: "me",
        q: query,
        maxResults,
      });

      const messages = response.data.messages || [];
      const messageDetails = await Promise.all(
        messages.map((msg) => this.getMessageMetadata(msg.id!)),
      );

      return await Promise.all(messageDetails.map((msg) => this.extractEmailMetadata(msg)));
    } catch (error) {
      console.error("Failed to search Gmail messages:", error);
      throw error;
    }
  }

  async getLabels(): Promise<gmail_v1.Schema$Label[]> {
    await this.ensureInitialized();
    try {
      const response = await this.gmail.users.labels.list({
        userId: "me",
      });

      return response.data.labels || [];
    } catch (error) {
      console.error("Failed to get Gmail labels:", error);
      throw error;
    }
  }

  private createEmailRaw(options: SendEmailOptions): string {
    const boundary = "boundary" + Date.now().toString();
    const toList = Array.isArray(options.to) ? options.to : [options.to];
    const ccList = options.cc ? (Array.isArray(options.cc) ? options.cc : [options.cc]) : [];
    const bccList = options.bcc ? (Array.isArray(options.bcc) ? options.bcc : [options.bcc]) : [];

    let email = [
      `Content-Type: multipart/mixed; boundary="${boundary}"`,
      "MIME-Version: 1.0",
      `To: ${toList.join(", ")}`,
      `Subject: ${options.subject}`,
    ];

    if (ccList.length > 0) email.push(`Cc: ${ccList.join(", ")}`);
    if (bccList.length > 0) email.push(`Bcc: ${bccList.join(", ")}`);
    if (options.replyTo) email.push(`Reply-To: ${options.replyTo}`);

    email.push("", `--${boundary}`);

    // Add the email body
    email.push(
      `Content-Type: ${options.isHtml ? "text/html" : "text/plain"}; charset="UTF-8"`,
      "MIME-Version: 1.0",
      "Content-Transfer-Encoding: 7bit",
      "",
      options.body,
      "",
    );

    // Add attachments if any
    if (options.attachments?.length) {
      for (const attachment of options.attachments) {
        const content = Buffer.isBuffer(attachment.content)
          ? attachment.content.toString("base64")
          : Buffer.from(attachment.content).toString("base64");

        email.push(
          `--${boundary}`,
          "Content-Type: " + (attachment.contentType || "application/octet-stream"),
          "MIME-Version: 1.0",
          "Content-Transfer-Encoding: base64",
          `Content-Disposition: attachment; filename="${attachment.filename}"`,
          "",
          content.replace(/(.{76})/g, "$1\n"),
          "",
        );
      }
    }

    email.push(`--${boundary}--`);

    return Buffer.from(email.join("\r\n")).toString("base64url");
  }

  async sendEmail(options: SendEmailOptions): Promise<string> {
    await this.ensureInitialized();
    try {
      // Validate email addresses
      validateEmailList(options.to);
      validateEmailList(options.cc);
      validateEmailList(options.bcc);

      const raw = this.createEmailRaw(options);
      const response = await this.gmail.users.messages.send({
        userId: "me",
        requestBody: { raw },
      });

      return response.data.id!;
    } catch (error: any) {
      console.error("Failed to send email:", error);
      throw error;
    }
  }

  async createDraft(options: DraftEmailOptions): Promise<string> {
    await this.ensureInitialized();
    try {
      // Validate email addresses
      validateEmailList(options.to);
      validateEmailList(options.cc);
      validateEmailList(options.bcc);

      const raw = this.createEmailRaw(options);
      const response = await this.gmail.users.drafts.create({
        userId: "me",
        requestBody: {
          message: { raw },
        },
      });

      return response.data.id!;
    } catch (error: any) {
      console.error("Failed to create draft:", error);
      throw error;
    }
  }

  async updateDraft(options: DraftEmailOptions): Promise<string> {
    if (!options.id) {
      throw new Error("Draft ID is required for updating");
    }

    await this.ensureInitialized();
    try {
      // Validate email addresses
      validateEmailList(options.to);
      validateEmailList(options.cc);
      validateEmailList(options.bcc);

      const raw = this.createEmailRaw(options);
      const response = await this.gmail.users.drafts.update({
        userId: "me",
        id: options.id,
        requestBody: {
          message: { raw },
        },
      });

      return response.data.id!;
    } catch (error: any) {
      console.error("Failed to update draft:", error);
      throw error;
    }
  }

  async listDrafts(maxResults: number = 10): Promise<EmailMetadata[]> {
    await this.ensureInitialized();
    try {
      const response = await this.gmail.users.drafts.list({
        userId: "me",
        maxResults,
      });

      const drafts = response.data.drafts || [];
      const messageDetails = await Promise.all(
        drafts.map((draft) => this.getMessageMetadata(draft.message!.id!)),
      );

      return await Promise.all(messageDetails.map((msg) => this.extractEmailMetadata(msg)));
    } catch (error) {
      console.error("Failed to list drafts:", error);
      throw error;
    }
  }

  async deleteDraft(draftId: string): Promise<void> {
    await this.ensureInitialized();
    try {
      await this.gmail.users.drafts.delete({
        userId: "me",
        id: draftId,
      });
    } catch (error) {
      console.error("Failed to delete draft:", error);
      throw error;
    }
  }

  async getDraft(draftId: string): Promise<EmailMetadata & { body: string }> {
    await this.ensureInitialized();
    try {
      const response = await this.gmail.users.drafts.get({
        userId: "me",
        id: draftId,
        format: "full",
      });

      if (!response.data.message) {
        throw new Error("Draft message not found");
      }

      const metadata = await this.extractEmailMetadata(response.data.message);
      let body = "";

      // Extract message body
      const message = response.data.message;
      if (message.payload) {
        if (message.payload.body?.data) {
          body = Buffer.from(message.payload.body.data, "base64").toString("utf8");
        } else if (message.payload.parts) {
          const textPart = message.payload.parts.find(
            (part) => part.mimeType === "text/plain" || part.mimeType === "text/html",
          );
          if (textPart?.body?.data) {
            body = Buffer.from(textPart.body.data, "base64").toString("utf8");
          }
        }
      }

      return {
        ...metadata,
        body,
      };
    } catch (error) {
      console.error("Failed to get draft:", error);
      throw error;
    }
  }

  async modifyMessage(
    messageId: string,
    options: {
      addLabelIds?: string[];
      removeLabelIds?: string[];
    },
  ): Promise<EmailMetadata> {
    await this.ensureInitialized();
    try {
      const response = await this.gmail.users.messages.modify({
        userId: "me",
        id: messageId,
        requestBody: options,
      });

      return this.extractEmailMetadata(response.data);
    } catch (error) {
      console.error("Failed to modify message:", error);
      throw error;
    }
  }

  async trashMessage(messageId: string): Promise<void> {
    await this.ensureInitialized();
    try {
      await this.gmail.users.messages.trash({
        userId: "me",
        id: messageId,
      });
    } catch (error) {
      console.error("Failed to trash message:", error);
      throw error;
    }
  }

  async untrashMessage(messageId: string): Promise<void> {
    await this.ensureInitialized();
    try {
      await this.gmail.users.messages.untrash({
        userId: "me",
        id: messageId,
      });
    } catch (error) {
      console.error("Failed to untrash message:", error);
      throw error;
    }
  }

  async deleteMessage(messageId: string): Promise<void> {
    await this.ensureInitialized();
    try {
      await this.gmail.users.messages.delete({
        userId: "me",
        id: messageId,
      });
    } catch (error) {
      console.error("Failed to delete message:", error);
      throw error;
    }
  }

  async createLabel(
    name: string,
    options: {
      textColor?: string;
      backgroundColor?: string;
      messageListVisibility?: "hide" | "show";
      labelListVisibility?: "labelHide" | "labelShow" | "labelShowIfUnread";
    } = {},
  ): Promise<gmail_v1.Schema$Label> {
    await this.ensureInitialized();
    try {
      const response = await this.gmail.users.labels.create({
        userId: "me",
        requestBody: {
          name,
          messageListVisibility: options.messageListVisibility,
          labelListVisibility: options.labelListVisibility,
          color:
            options.textColor || options.backgroundColor
              ? {
                  textColor: options.textColor,
                  backgroundColor: options.backgroundColor,
                }
              : undefined,
        },
      });

      // Update label cache
      this.labelCache.set(response.data.id!, response.data);
      return response.data;
    } catch (error) {
      console.error("Failed to create label:", error);
      throw error;
    }
  }

  async deleteLabel(labelId: string): Promise<void> {
    await this.ensureInitialized();
    try {
      await this.gmail.users.labels.delete({
        userId: "me",
        id: labelId,
      });
      // Remove from cache
      this.labelCache.delete(labelId);
    } catch (error) {
      console.error("Failed to delete label:", error);
      throw error;
    }
  }

  async replyEmail(messageId: string, body: string, isHtml: boolean = false): Promise<string> {
    await this.ensureInitialized();
    try {
      // Get the original message to extract threading information
      const originalMessage: gmail_v1.Schema$Message = (
        await this.gmail.users.messages.get({
          userId: "me",
          id: messageId,
          format: "metadata",
          metadataHeaders: ["Subject", "Message-ID", "References", "From", "To"],
        })
      ).data;

      const headers = originalMessage.payload?.headers || [];
      const subjectHeader = headers.find(
        (h: gmail_v1.Schema$MessagePartHeader) => h.name === "Subject",
      );
      const messageIdHeader = headers.find(
        (h: gmail_v1.Schema$MessagePartHeader) => h.name === "Message-ID",
      );
      const referencesHeader = headers.find(
        (h: gmail_v1.Schema$MessagePartHeader) => h.name === "References",
      );
      const fromHeader = headers.find((h: gmail_v1.Schema$MessagePartHeader) => h.name === "From");
      const toHeader = headers.find((h: gmail_v1.Schema$MessagePartHeader) => h.name === "To");

      const subject = subjectHeader?.value || "";
      const originalMessageId = messageIdHeader?.value || "";
      const references = referencesHeader?.value || "";
      const from = fromHeader?.value || "";
      const to = toHeader?.value || "";

      // Build References header for proper threading
      const newReferences = references ? `${references} ${originalMessageId}` : originalMessageId;

      // Create email with proper threading headers
      const email = [
        `Content-Type: ${isHtml ? "text/html" : "text/plain"}; charset="UTF-8"`,
        "MIME-Version: 1.0",
        `Subject: ${subject.startsWith("Re:") ? subject : `Re: ${subject}`}`,
        `To: ${from}`,
        `References: ${newReferences}`,
        `In-Reply-To: ${originalMessageId}`,
        "",
        body,
      ].join("\r\n");

      const raw = Buffer.from(email).toString("base64url");

      const response = await this.gmail.users.messages.send({
        userId: "me",
        requestBody: {
          raw,
          threadId: originalMessage.threadId,
        },
      });

      return response.data.id!;
    } catch (error) {
      console.error("Failed to reply to email:", error);
      throw error;
    }
  }
}

```

--------------------------------------------------------------------------------
/src/services/__tests__/gmail-service.test.ts:
--------------------------------------------------------------------------------

```typescript
import { jest } from "@jest/globals";
import { google } from "googleapis";
import { GmailService } from "../gmail-service";
import { GoogleAuthService } from "../google-auth-service";
import { gmail_v1 } from "googleapis";

// Create a test class that exposes waitForInit
class TestGmailService extends GmailService {
  public async testInit(): Promise<void> {
    await this.waitForInit();
  }
}

jest.mock("googleapis");
jest.mock("../google-auth-service");

describe("GmailService", () => {
  let service: TestGmailService;
  let mockGmailAPI: any;
  let mockAuth: jest.Mocked<GoogleAuthService>;
  let mockMessage: any;
  let mockMessageMetadata: any;

  beforeEach(async () => {
    jest.clearAllMocks();

    mockMessage = {
      id: "1",
      threadId: "thread1",
      snippet: "Test email",
      from: {
        name: "Test Sender",
        email: "[email protected]",
      },
      to: [
        {
          name: "Test Recipient",
          email: "[email protected]",
        },
      ],
      subject: "Test Subject",
      date: new Date("2025-01-14T11:47:39.417Z"),
      isUnread: false,
      isImportant: false,
      hasAttachments: false,
      labels: [
        {
          id: "INBOX",
          name: "INBOX",
        },
      ],
    };

    mockMessageMetadata = { ...mockMessage }; // Create metadata version without body
    mockMessage.body = "Test body"; // Add body only to full message version

    // Mock Gmail API methods
    mockGmailAPI = {
      users: {
        messages: {
          list: jest.fn(),
          get: jest.fn(),
          modify: jest.fn(),
          trash: jest.fn(),
          untrash: jest.fn(),
          delete: jest.fn(),
          send: jest.fn(),
        },
        labels: {
          list: jest.fn(),
          create: jest.fn(),
          delete: jest.fn(),
        },
        drafts: {
          create: jest.fn(),
          update: jest.fn(),
          list: jest.fn(),
          delete: jest.fn(),
        },
      },
    };

    // Set up default mock responses
    mockGmailAPI.users.messages.send.mockResolvedValue({
      data: { id: "msg1" },
    });

    mockGmailAPI.users.messages.list.mockResolvedValue({
      data: {
        messages: [{ id: "1" }],
      },
    });

    mockGmailAPI.users.messages.get.mockImplementation(
      (params: { format?: string }) => {
        if (params.format === "metadata") {
          return Promise.resolve({
            data: {
              id: "1",
              threadId: "thread1",
              labelIds: ["INBOX"],
              snippet: "Test email",
              payload: {
                headers: [
                  { name: "Subject", value: "Test Subject" },
                  { name: "From", value: "Test Sender <[email protected]>" },
                  {
                    name: "To",
                    value: "Test Recipient <[email protected]>",
                  },
                  { name: "Date", value: "2025-01-14T11:47:39.417Z" },
                ],
              },
            },
          });
        } else {
          return Promise.resolve({
            data: {
              id: "1",
              threadId: "thread1",
              labelIds: ["INBOX"],
              snippet: "Test email",
              payload: {
                headers: [
                  { name: "Subject", value: "Test Subject" },
                  { name: "From", value: "Test Sender <[email protected]>" },
                  {
                    name: "To",
                    value: "Test Recipient <[email protected]>",
                  },
                  { name: "Date", value: "2025-01-14T11:47:39.417Z" },
                ],
                parts: [
                  {
                    mimeType: "text/plain",
                    body: { data: Buffer.from("Test body").toString("base64") },
                  },
                ],
              },
            },
          });
        }
      }
    );

    mockGmailAPI.users.messages.modify.mockResolvedValue({
      data: {
        id: "1",
        threadId: "thread1",
        labelIds: ["Label_1"],
        snippet: "Test email",
        payload: {
          headers: [
            { name: "Subject", value: "Test Subject" },
            { name: "From", value: "Test Sender <[email protected]>" },
            { name: "To", value: "Test Recipient <[email protected]>" },
            { name: "Date", value: "2025-01-14T11:47:39.417Z" },
          ],
        },
      },
    });

    mockGmailAPI.users.drafts.create.mockResolvedValue({
      data: { id: "draft1" },
    });

    mockGmailAPI.users.drafts.update.mockResolvedValue({
      data: { id: "draft1" },
    });

    mockGmailAPI.users.drafts.list.mockResolvedValue({
      data: {
        drafts: [
          {
            id: "draft1",
            message: {
              id: "1",
              threadId: "thread1",
              labelIds: ["DRAFT"],
              snippet: "Test email",
              payload: {
                headers: [
                  { name: "Subject", value: "Test Subject" },
                  { name: "From", value: "Test Sender <[email protected]>" },
                  {
                    name: "To",
                    value: "Test Recipient <[email protected]>",
                  },
                  { name: "Date", value: "2025-01-14T11:47:39.417Z" },
                ],
              },
            },
          },
        ],
      },
    });

    mockGmailAPI.users.labels.list.mockResolvedValue({
      data: {
        labels: [{ id: "INBOX", name: "INBOX" }],
      },
    });

    mockGmailAPI.users.labels.create.mockResolvedValue({
      data: { id: "new-label", name: "New Label" },
    });

    (google.gmail as jest.Mock).mockReturnValue(mockGmailAPI);

    // Mock auth service
    mockAuth = {
      initialize: jest.fn().mockImplementation(() => Promise.resolve()),
      authenticate: jest.fn().mockImplementation(() => Promise.resolve()),
      getAuth: jest.fn(),
      saveToken: jest.fn().mockImplementation(() => Promise.resolve()),
      oAuth2Client: undefined,
      authUrl: "",
    } as unknown as jest.Mocked<GoogleAuthService>;

    (GoogleAuthService.getInstance as jest.Mock).mockReturnValue(mockAuth);

    // Create service instance and wait for initialization
    service = new TestGmailService();
    await service.testInit();
  });

  describe("Email Validation", () => {
    it("should validate correct email addresses", async () => {
      await expect(
        service.sendEmail({
          to: "[email protected]",
          subject: "Test",
          body: "Test",
        })
      ).resolves.toBeDefined();
    });

    it("should reject invalid email addresses", async () => {
      await expect(
        service.sendEmail({
          to: "invalid-email",
          subject: "Test",
          body: "Test",
        })
      ).rejects.toThrow("Invalid email address");
    });

    it("should validate multiple email addresses", async () => {
      await expect(
        service.sendEmail({
          to: ["[email protected]", "[email protected]"],
          subject: "Test",
          body: "Test",
        })
      ).resolves.toBeDefined();
    });

    it("should handle malformed email addresses", async () => {
      const malformedEmail = "[email protected]"; // Without angle brackets
      mockGmailAPI.users.messages.send.mockResolvedValueOnce({
        data: { id: "123" },
      });
      await expect(
        service.sendEmail({
          to: malformedEmail,
          subject: "Test",
          body: "Test body",
        })
      ).resolves.toBeDefined();
      expect(mockGmailAPI.users.messages.send).toHaveBeenCalled();
    });

    it("should handle email addresses with display names", async () => {
      const emailWithName = "[email protected]"; // Without display name
      mockGmailAPI.users.messages.send.mockResolvedValueOnce({
        data: { id: "123" },
      });
      await expect(
        service.sendEmail({
          to: emailWithName,
          subject: "Test",
          body: "Test body",
        })
      ).resolves.toBeDefined();
      expect(mockGmailAPI.users.messages.send).toHaveBeenCalled();
    });
  });

  describe("Message Operations", () => {
    it("should list messages", async () => {
      const result = await service.listMessages();
      expect(result).toEqual([mockMessageMetadata]);
    });

    it("should handle empty message list", async () => {
      mockGmailAPI.users.messages.list.mockResolvedValueOnce({
        data: { messages: [] },
      });
      const result = await service.listMessages();
      expect(result).toEqual([]);
    });

    it("should get message with simple body", async () => {
      mockGmailAPI.users.messages.get.mockResolvedValueOnce({
        data: {
          id: "1",
          threadId: "thread1",
          labelIds: ["INBOX"],
          snippet: "Test email",
          payload: {
            headers: [
              { name: "Subject", value: "Test Subject" },
              { name: "From", value: "Test Sender <[email protected]>" },
              { name: "To", value: "Test Recipient <[email protected]>" },
              { name: "Date", value: "2025-01-14T11:47:39.417Z" },
            ],
            body: {
              data: Buffer.from("Test body").toString("base64"),
            },
          },
        },
      });

      const result = await service.getMessage("1");
      expect(result).toEqual(mockMessage);
    });

    it("should get message with multipart body", async () => {
      const result = await service.getMessage("1");
      expect(result).toEqual(mockMessage);
    });

    it("should handle message without body", async () => {
      mockGmailAPI.users.messages.get.mockResolvedValueOnce({
        data: {
          id: "1",
          threadId: "thread1",
          labelIds: ["INBOX"],
          snippet: "Test email",
          payload: {
            headers: [
              { name: "Subject", value: "Test Subject" },
              { name: "From", value: "Test Sender <[email protected]>" },
              { name: "To", value: "Test Recipient <[email protected]>" },
              { name: "Date", value: "2025-01-14T11:47:39.417Z" },
            ],
          },
        },
      });

      const result = await service.getMessage("1");
      expect(result.body).toBe("");
    });

    it("should search messages", async () => {
      const result = await service.searchMessages("test query");
      expect(result).toEqual([mockMessageMetadata]);
    });

    it("should modify message labels", async () => {
      await service.modifyMessage("1", {
        addLabelIds: ["Label_1"],
        removeLabelIds: ["Label_2"],
      });
      expect(mockGmailAPI.users.messages.modify).toHaveBeenCalledWith({
        userId: "me",
        id: "1",
        requestBody: {
          addLabelIds: ["Label_1"],
          removeLabelIds: ["Label_2"],
        },
      });
    });

    it("should trash message", async () => {
      await service.trashMessage("1");
      expect(mockGmailAPI.users.messages.trash).toHaveBeenCalledWith({
        userId: "me",
        id: "1",
      });
    });

    it("should untrash message", async () => {
      await service.untrashMessage("1");
      expect(mockGmailAPI.users.messages.untrash).toHaveBeenCalledWith({
        userId: "me",
        id: "1",
      });
    });

    it("should delete message", async () => {
      await service.deleteMessage("1");
      expect(mockGmailAPI.users.messages.delete).toHaveBeenCalledWith({
        userId: "me",
        id: "1",
      });
    });

    it("should handle errors in message metadata retrieval", async () => {
      mockGmailAPI.users.messages.get.mockRejectedValueOnce(
        new Error("Metadata Error")
      );
      await expect(service.getMessage("1")).rejects.toThrow("Metadata Error");
    });

    it("should create email with CC and BCC", async () => {
      await service.sendEmail({
        to: "[email protected]",
        cc: ["[email protected]", "[email protected]"],
        bcc: "[email protected]",
        subject: "Test",
        body: "Test body",
      });
      expect(mockGmailAPI.users.messages.send).toHaveBeenCalled();
    });

    it("should create email with attachments", async () => {
      await service.sendEmail({
        to: "[email protected]",
        subject: "Test with attachment",
        body: "Test body",
        attachments: [
          {
            filename: "test.txt",
            content: "Test content",
            contentType: "text/plain",
          },
        ],
      });
      expect(mockGmailAPI.users.messages.send).toHaveBeenCalled();
    });

    it("should create HTML email with reply-to", async () => {
      await service.sendEmail({
        to: "[email protected]",
        subject: "Test HTML",
        body: "<p>Test body</p>",
        isHtml: true,
        replyTo: "[email protected]",
      });
      expect(mockGmailAPI.users.messages.send).toHaveBeenCalled();
    });
  });

  describe("Draft Operations", () => {
    it("should create draft", async () => {
      const result = await service.createDraft({
        to: "[email protected]",
        subject: "Test Draft",
        body: "Draft body",
      });
      expect(result).toBe("draft1");
    });

    it("should update draft", async () => {
      const result = await service.updateDraft({
        id: "draft1",
        to: "[email protected]",
        subject: "Updated Draft",
        body: "Updated body",
      });
      expect(result).toBe("draft1");
    });

    it("should list drafts", async () => {
      const result = await service.listDrafts();
      expect(result).toEqual([mockMessageMetadata]);
    });

    it("should delete draft", async () => {
      await service.deleteDraft("draft1");
      expect(mockGmailAPI.users.drafts.delete).toHaveBeenCalledWith({
        userId: "me",
        id: "draft1",
      });
    });

    it("should handle errors in draft creation", async () => {
      mockGmailAPI.users.drafts.create.mockRejectedValueOnce(
        new Error("Draft Error")
      );
      await expect(
        service.createDraft({
          to: "[email protected]",
          subject: "Test",
          body: "Test",
        })
      ).rejects.toThrow("Draft Error");
    });

    it("should handle errors in draft update", async () => {
      mockGmailAPI.users.drafts.update.mockRejectedValueOnce(
        new Error("Update Error")
      );
      await expect(
        service.updateDraft({
          id: "draft1",
          to: "[email protected]",
          subject: "Test",
          body: "Test",
        })
      ).rejects.toThrow("Update Error");
    });

    it("should handle errors in draft deletion", async () => {
      mockGmailAPI.users.drafts.delete.mockRejectedValueOnce(
        new Error("Delete Error")
      );
      await expect(service.deleteDraft("draft1")).rejects.toThrow(
        "Delete Error"
      );
    });

    it("should handle empty draft list", async () => {
      mockGmailAPI.users.drafts.list.mockResolvedValueOnce({
        data: {}, // No drafts property
      });
      const result = await service.listDrafts();
      expect(result).toEqual([]);
    });

    it("should handle draft creation with minimal options", async () => {
      await service.createDraft({
        to: "[email protected]",
        subject: "Test",
        body: "Test",
      });
      expect(mockGmailAPI.users.drafts.create).toHaveBeenCalled();
    });

    it("should handle draft metadata", async () => {
      const draftId = "draft123";
      const emailOptions = {
        to: "[email protected]",
        subject: "Test Draft",
        body: "Test body",
      };
      mockGmailAPI.users.drafts.create.mockResolvedValueOnce({
        data: {
          id: draftId,
          message: {
            id: "msg123",
            threadId: "thread123",
          },
        },
      });
      await service.createDraft(emailOptions);
      expect(mockGmailAPI.users.drafts.create).toHaveBeenCalled();
    });
  });

  describe("Label Operations", () => {
    it("should list labels", async () => {
      const result = await service.getLabels();
      expect(result).toEqual([{ id: "INBOX", name: "INBOX" }]);
    });

    it("should create label", async () => {
      const result = await service.createLabel("New Label", {
        textColor: "#000000",
        backgroundColor: "#ffffff",
        messageListVisibility: "show",
        labelListVisibility: "labelShow",
      });
      expect(result).toEqual({ id: "new-label", name: "New Label" });
    });

    it("should delete label", async () => {
      await service.deleteLabel("label1");
      expect(mockGmailAPI.users.labels.delete).toHaveBeenCalledWith({
        userId: "me",
        id: "label1",
      });
    });

    it("should handle errors in label creation", async () => {
      mockGmailAPI.users.labels.create.mockRejectedValueOnce(
        new Error("Label Error")
      );
      await expect(service.createLabel("Test Label")).rejects.toThrow(
        "Label Error"
      );
    });

    it("should handle errors in label deletion", async () => {
      mockGmailAPI.users.labels.delete.mockRejectedValueOnce(
        new Error("Delete Error")
      );
      await expect(service.deleteLabel("label1")).rejects.toThrow(
        "Delete Error"
      );
    });

    it("should handle empty label list", async () => {
      mockGmailAPI.users.labels.list.mockResolvedValueOnce({
        data: {}, // No labels property
      });
      const result = await service.getLabels();
      expect(result).toEqual([]);
    });

    it("should handle label creation with all options", async () => {
      await service.createLabel("Test Label", {
        textColor: "#000000",
        backgroundColor: "#ffffff",
        messageListVisibility: "show",
        labelListVisibility: "labelShow",
      });
      expect(mockGmailAPI.users.labels.create).toHaveBeenCalledWith({
        userId: "me",
        requestBody: {
          name: "Test Label",
          color: {
            textColor: "#000000",
            backgroundColor: "#ffffff",
          },
          messageListVisibility: "show",
          labelListVisibility: "labelShow",
        },
      });
    });

    it("should handle label creation with minimal options", async () => {
      await service.createLabel("Test Label");
      expect(mockGmailAPI.users.labels.create).toHaveBeenCalledWith({
        userId: "me",
        requestBody: {
          name: "Test Label",
        },
      });
    });
  });

  describe("Error Handling", () => {
    it("should handle API errors in listMessages", async () => {
      mockGmailAPI.users.messages.list.mockRejectedValue(
        new Error("API Error")
      );
      await expect(service.listMessages()).rejects.toThrow("API Error");
    });

    it("should handle API errors in getMessage", async () => {
      mockGmailAPI.users.messages.get.mockRejectedValue(new Error("API Error"));
      await expect(service.getMessage("1")).rejects.toThrow("API Error");
    });

    it("should handle API errors in searchMessages", async () => {
      mockGmailAPI.users.messages.list.mockRejectedValue(
        new Error("API Error")
      );
      await expect(service.searchMessages("query")).rejects.toThrow(
        "API Error"
      );
    });
  });

  describe("Message Modification", () => {
    it("should handle errors in message modification", async () => {
      mockGmailAPI.users.messages.modify.mockRejectedValueOnce(
        new Error("Modify Error")
      );
      await expect(
        service.modifyMessage("1", { addLabelIds: ["Label_1"] })
      ).rejects.toThrow("Modify Error");
    });

    it("should handle errors in message trash operation", async () => {
      mockGmailAPI.users.messages.trash.mockRejectedValueOnce(
        new Error("Trash Error")
      );
      await expect(service.trashMessage("1")).rejects.toThrow("Trash Error");
    });

    it("should handle errors in message untrash operation", async () => {
      mockGmailAPI.users.messages.untrash.mockRejectedValueOnce(
        new Error("Untrash Error")
      );
      await expect(service.untrashMessage("1")).rejects.toThrow(
        "Untrash Error"
      );
    });

    it("should handle errors in message delete operation", async () => {
      mockGmailAPI.users.messages.delete.mockRejectedValueOnce(
        new Error("Delete Error")
      );
      await expect(service.deleteMessage("1")).rejects.toThrow("Delete Error");
    });
  });

  describe("Email Parsing", () => {
    it("should handle errors in email parsing", async () => {
      const messageId = "123";
      mockGmailAPI.users.messages.get.mockRejectedValueOnce(
        new Error("Parse Error")
      );
      await expect(service.getMessage(messageId)).rejects.toThrow(
        "Parse Error"
      );
    });

    it("should handle malformed email parsing", async () => {
      const messageId = "123";
      mockGmailAPI.users.messages.get.mockResolvedValue({
        data: {
          id: messageId,
          threadId: undefined,
          labelIds: [],
          snippet: "",
          payload: {
            headers: [],
            parts: [],
          },
        },
      });
      const result = await service.getMessage(messageId);
      expect(result).toEqual({
        id: messageId,
        threadId: undefined,
        subject: "(no subject)",
        from: { email: "" },
        to: [{ email: "" }],
        date: expect.any(Date),
        body: "",
        snippet: "",
        labels: [],
        isUnread: false,
        isImportant: false,
        hasAttachments: false,
      });
    });
  });

  describe("Label Handling", () => {
    it("should handle errors in label handling", async () => {
      const labelName = "Test Label";
      mockGmailAPI.users.labels.create.mockRejectedValueOnce(
        new Error("Label Error")
      );
      await expect(service.createLabel(labelName)).rejects.toThrow(
        "Label Error"
      );
    });

    it("should handle label visibility options", async () => {
      const labelName = "Test Label";
      mockGmailAPI.users.labels.create.mockResolvedValueOnce({
        data: {
          id: "Label_123",
          name: labelName,
          labelListVisibility: "labelShow",
          messageListVisibility: "show",
        },
      });
      await service.createLabel(labelName, {
        labelListVisibility: "labelShow",
        messageListVisibility: "show",
      });
      expect(mockGmailAPI.users.labels.create).toHaveBeenCalledWith({
        userId: "me",
        requestBody: {
          name: labelName,
          labelListVisibility: "labelShow",
          messageListVisibility: "show",
        },
      });
    });
  });

  describe("Draft Operations", () => {
    it("should handle errors in draft operations with metadata", async () => {
      const draftId = "draft123";
      const emailOptions = {
        to: "[email protected]",
        subject: "Test Draft",
        body: "Test body",
      };
      mockGmailAPI.users.drafts.create.mockRejectedValueOnce(
        new Error("Draft Error")
      );
      await expect(service.createDraft(emailOptions)).rejects.toThrow(
        "Draft Error"
      );
    });

    it("should handle draft metadata", async () => {
      const draftId = "draft123";
      const emailOptions = {
        to: "[email protected]",
        subject: "Test Draft",
        body: "Test body",
      };
      mockGmailAPI.users.drafts.create.mockResolvedValueOnce({
        data: {
          id: draftId,
          message: {
            id: "msg123",
            threadId: "thread123",
          },
        },
      });
      await service.createDraft(emailOptions);
      expect(mockGmailAPI.users.drafts.create).toHaveBeenCalled();
    });
  });
});

```
Page 2/2FirstPrevNextLast