This is page 2 of 2. Use http://codebase.md/ejb503/systemprompt-mcp-gmail?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(/'/g, "'").replace(/"/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();
});
});
});
```