Gmail MCP Server
by cablate
- src
- providers
import { OAuth2Client } from "google-auth-library";
import { gmail_v1, google } from "googleapis";
import { IAttachment, IEmail, IEmailToSend, IMailProvider } from "../types/mail.types";
export class GmailProvider implements IMailProvider {
private auth: OAuth2Client;
private gmail: gmail_v1.Gmail;
constructor(credentials: { clientId: string; clientSecret: string; refreshToken: string }) {
this.auth = new google.auth.OAuth2(credentials.clientId, credentials.clientSecret);
this.auth.setCredentials({
refresh_token: credentials.refreshToken,
});
this.gmail = google.gmail({ version: "v1", auth: this.auth });
}
async connect(): Promise<void> {
try {
// Test connection
await this.gmail.users.getProfile({ userId: "me" });
} catch (error: any) {
throw new Error(`Failed to connect to Gmail: ${error.message}`);
}
}
async getEmails(query = "", maxResults = 10): Promise<IEmail[]> {
try {
const response = await this.gmail.users.messages.list({
userId: "me",
q: query,
maxResults: maxResults,
});
const emails: IEmail[] = [];
for (const message of response.data.messages || []) {
if (!message.id) continue;
const fullMessage = await this.gmail.users.messages.get({
userId: "me",
id: message.id,
});
if (!fullMessage.data) continue;
const email = await this.parseGmailMessage(fullMessage.data);
emails.push(email);
}
return emails;
} catch (error: any) {
throw new Error(`Failed to fetch emails: ${error.message}`);
}
}
async sendEmail(email: IEmailToSend): Promise<void> {
try {
const message = await this.createMimeMessage(email);
await this.gmail.users.messages.send({
userId: "me",
requestBody: {
raw: message,
},
});
} catch (error: any) {
throw new Error(`Failed to send email: ${error.message}`);
}
}
private async parseGmailMessage(message: gmail_v1.Schema$Message): Promise<IEmail> {
if (!message.id || !message.threadId || !message.payload) {
throw new Error("Invalid message format");
}
const headers = message.payload.headers || [];
const getHeader = (name: string) => {
const header = headers.find((h) => h.name?.toLowerCase() === name.toLowerCase());
return header?.value || "";
};
const parts = this.flattenParts(message.payload);
const attachments: IAttachment[] = [];
let textContent = "";
let htmlContent = "";
for (const part of parts) {
if (part.filename && part.body?.attachmentId) {
try {
const attachment = await this.getAttachment(message.id, part.body.attachmentId);
if (attachment.data) {
attachments.push({
filename: part.filename,
content: Buffer.from(attachment.data, "base64"),
contentType: part.mimeType || "application/octet-stream",
});
}
} catch (error) {
console.warn(`Failed to get attachment ${part.filename}: ${error}`);
}
} else if (part.mimeType === "text/plain" && part.body?.data) {
textContent = Buffer.from(part.body.data, "base64").toString();
} else if (part.mimeType === "text/html" && part.body?.data) {
htmlContent = Buffer.from(part.body.data, "base64").toString();
}
}
return {
id: message.id,
threadId: message.threadId,
subject: getHeader("subject"),
from: getHeader("from"),
to: getHeader("to")
.split(",")
.map((email: string) => email.trim()),
cc: getHeader("cc")
? getHeader("cc")
.split(",")
.map((email: string) => email.trim())
: [],
bcc: [],
content: {
text: textContent,
html: htmlContent,
},
attachments,
date: new Date(parseInt(message.internalDate || "0")),
labels: message.labelIds || [],
};
}
private flattenParts(part?: gmail_v1.Schema$MessagePart): gmail_v1.Schema$MessagePart[] {
if (!part) return [];
const parts: gmail_v1.Schema$MessagePart[] = [];
if (part.parts) {
part.parts.forEach((p) => parts.push(...this.flattenParts(p)));
}
parts.push(part);
return parts;
}
private async getAttachment(messageId: string, attachmentId: string): Promise<gmail_v1.Schema$MessagePartBody> {
try {
const response = await this.gmail.users.messages.attachments.get({
userId: "me",
messageId,
id: attachmentId,
});
if (!response.data) {
throw new Error("Failed to get attachment data");
}
return response.data;
} catch (error: any) {
throw new Error(`Failed to get attachment: ${error.message}`);
}
}
private async createMimeMessage(email: IEmailToSend): Promise<string> {
const boundary = "boundary_" + Date.now().toString(16);
const mimeContent: string[] = [];
// Headers
mimeContent.push("MIME-Version: 1.0");
mimeContent.push(`To: ${email.to.join(", ")}`);
if (email.cc?.length) mimeContent.push(`Cc: ${email.cc.join(", ")}`);
if (email.bcc?.length) mimeContent.push(`Bcc: ${email.bcc.join(", ")}`);
mimeContent.push(`Subject: ${email.subject}`);
mimeContent.push(`Content-Type: multipart/mixed; boundary=${boundary}`);
mimeContent.push("");
// Text content
if (email.content.text) {
mimeContent.push(`--${boundary}`);
mimeContent.push("Content-Type: text/plain; charset=UTF-8");
mimeContent.push("");
mimeContent.push(email.content.text);
}
// HTML content
if (email.content.html) {
mimeContent.push(`--${boundary}`);
mimeContent.push("Content-Type: text/html; charset=UTF-8");
mimeContent.push("");
mimeContent.push(email.content.html);
}
// Attachments
if (email.attachments) {
for (const attachment of email.attachments) {
mimeContent.push(`--${boundary}`);
mimeContent.push(`Content-Type: ${attachment.contentType}`);
mimeContent.push(`Content-Disposition: attachment; filename="${attachment.filename}"`);
mimeContent.push("Content-Transfer-Encoding: base64");
mimeContent.push("");
mimeContent.push(attachment.content.toString("base64"));
}
}
mimeContent.push(`--${boundary}--`);
return Buffer.from(mimeContent.join("\r\n")).toString("base64url");
}
}