systemprompt-mcp-interview
Version:
A specialized Model Context Protocol (MCP) server that enables AI-powered interview roleplay scenarios
491 lines • 18.6 kB
JavaScript
import { google } from "googleapis";
import { GoogleBaseService } from "./google-base-service.js";
const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
function validateEmail(email) {
return EMAIL_REGEX.test(email.trim());
}
function validateEmailList(emails) {
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 {
gmail;
labelCache = new Map();
gmailInitPromise;
constructor() {
super();
this.gmailInitPromise = this.initializeGmailClient();
}
async initializeGmailClient() {
await this.waitForInit();
this.gmail = google.gmail({ version: "v1", auth: this.auth.getAuth() });
}
// Helper method to ensure initialization is complete
async ensureInitialized() {
await this.gmailInitPromise;
}
async loadLabels() {
await this.ensureInitialized();
if (this.labelCache.size === 0) {
const labels = await this.getLabels();
labels.forEach((label) => {
this.labelCache.set(label.id, label);
});
}
}
parseEmailAddress(address) {
const match = address.match(/(?:"?([^"]*)"?\s)?(?:<)?(.+@[^>]+)(?:>)?/);
if (match) {
return {
name: match[1]?.trim(),
email: match[2].trim(),
};
}
return { email: address.trim() };
}
async getMessageMetadata(messageId) {
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;
}
}
async extractEmailMetadata(message) {
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 !== 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 = 100) {
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) {
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, maxResults = 10) {
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() {
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;
}
}
createEmailRaw(options) {
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) {
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) {
console.error("Failed to send email:", error);
throw error;
}
}
async createDraft(options) {
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) {
console.error("Failed to create draft:", error);
throw error;
}
}
async updateDraft(options) {
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) {
console.error("Failed to update draft:", error);
throw error;
}
}
async listDrafts(maxResults = 10) {
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) {
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) {
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, options) {
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) {
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) {
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) {
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, options = {}) {
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) {
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, body, isHtml = false) {
await this.ensureInitialized();
try {
// Get the original message to extract threading information
const originalMessage = (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) => h.name === "Subject");
const messageIdHeader = headers.find((h) => h.name === "Message-ID");
const referencesHeader = headers.find((h) => h.name === "References");
const fromHeader = headers.find((h) => h.name === "From");
const toHeader = headers.find((h) => 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;
}
}
}
//# sourceMappingURL=gmail-service.js.map