systemprompt-mcp-notion
Version:
A specialized Model Context Protocol (MCP) server that integrates Notion into your AI workflows. This server enables seamless access to Notion through MCP, allowing AI agents to interact with pages, databases, and comments.
393 lines (392 loc) • 14 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();
constructor() {
super();
this.gmail = google.gmail({ version: "v1", auth: this.auth.getAuth() });
}
async loadLabels() {
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) {
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 = 10) {
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) {
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) {
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() {
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) {
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) {
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");
}
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) {
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) {
try {
await this.gmail.users.drafts.delete({
userId: "me",
id: draftId,
});
}
catch (error) {
console.error("Failed to delete draft:", error);
throw error;
}
}
async modifyMessage(messageId, options) {
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) {
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) {
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) {
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 = {}) {
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) {
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;
}
}
}