Skip to main content
Glama
readEmails.ts9.88 kB
import { google } from "googleapis"; import ImapOriginal from "imap"; // @ts-ignore import ImapMkl from "imap-mkl"; import { simpleParser } from "mailparser"; import { getEmailAccounts, getAccountByEmail, getDefaultAccount } from "../utils/emailAccounts.js"; interface ReadEmailsArgs { limit: number; folder: string; unreadOnly: boolean; account?: string; // 可选:指定账户名称(qq, 163)或邮箱地址 } interface EmailMessage { id: string; threadId: string; from: string; to: string; subject: string; snippet: string; date: string; isUnread: boolean; body?: string; } function getEmailProvider(): string { return process.env.EMAIL_PROVIDER || "smtp"; } function getImapConfig(accountName?: string) { const accounts = getEmailAccounts(); // 如果指定了账户,使用指定的账户 if (accountName) { // 先尝试作为账户名称查找 let account = accounts.get(accountName.toLowerCase()); // 如果没找到,尝试作为邮箱地址查找 if (!account) { account = getAccountByEmail(accountName); } if (account) { return { user: account.imap.user, password: account.imap.pass, host: account.imap.host, port: account.imap.port, tls: account.imap.secure, tlsOptions: { rejectUnauthorized: false }, }; } } // 使用默认账户 const defaultAccountName = getDefaultAccount(); const defaultAccount = accounts.get(defaultAccountName); if (defaultAccount) { return { user: defaultAccount.imap.user, password: defaultAccount.imap.pass, host: defaultAccount.imap.host, port: defaultAccount.imap.port, tls: defaultAccount.imap.secure, tlsOptions: { rejectUnauthorized: false }, }; } // 回退到环境变量配置 return { user: process.env.IMAP_USER || process.env.SMTP_USER!, password: process.env.IMAP_PASS || process.env.SMTP_PASS!, host: process.env.IMAP_HOST || "imap.qq.com", port: parseInt(process.env.IMAP_PORT || "993"), tls: process.env.IMAP_SECURE !== "false", tlsOptions: { rejectUnauthorized: false }, }; } function getGmailConfig() { return { clientId: process.env.GMAIL_CLIENT_ID!, clientSecret: process.env.GMAIL_CLIENT_SECRET!, refreshToken: process.env.GMAIL_REFRESH_TOKEN!, accessToken: process.env.GMAIL_ACCESS_TOKEN, }; } async function readEmailsViaImap(args: ReadEmailsArgs): Promise<EmailMessage[]> { return new Promise((resolve, reject) => { const config = getImapConfig(args.account); // 检查是否是 163 邮箱,如果是则使用 imap-mkl 并添加 ID 信息 const is163 = config.host.includes('163.com'); let imap: any; if (is163) { const configWith163Id = { ...config, id: { name: 'email-mcp', version: '1.0.0', vendor: 'email-mcp-client', 'support-email': config.user } }; imap = new ImapMkl(configWith163Id); } else { imap = new ImapOriginal(config); } const emails: EmailMessage[] = []; imap.once("ready", () => { imap.openBox(args.folder, true, (err: any, box: any) => { if (err) { imap.end(); return reject(err); } const searchCriteria = args.unreadOnly ? ["UNSEEN"] : ["ALL"]; imap.search(searchCriteria, (err: any, results: any) => { if (err) { imap.end(); return reject(err); } if (!results || results.length === 0) { imap.end(); return resolve([]); } // Limit results const limitedResults = results.slice(-args.limit); const fetch = imap.fetch(limitedResults, { bodies: "", struct: true, }); fetch.on("message", (msg: any, seqno: number) => { msg.on("body", (stream: any) => { simpleParser(stream, async (err: any, parsed: any) => { if (err) return; const from = parsed.from?.text || ""; const to = parsed.to?.text || ""; const subject = parsed.subject || ""; const date = parsed.date?.toString() || ""; const textBody = parsed.text || ""; const htmlBody = parsed.html || ""; const body = textBody || htmlBody.toString().substring(0, 1000); emails.push({ id: seqno.toString(), threadId: parsed.messageId || seqno.toString(), from, to, subject, snippet: body.substring(0, 150), date, isUnread: args.unreadOnly, body: body.substring(0, 1000), }); }); }); }); fetch.once("error", (err: any) => { imap.end(); reject(err); }); fetch.once("end", () => { imap.end(); // Wait a bit for all messages to be parsed setTimeout(() => resolve(emails), 500); }); }); }); }); imap.once("error", (err: any) => { reject(err); }); imap.connect(); }); } async function getGmailClient() { const config = getGmailConfig(); if (!config.clientId || !config.clientSecret || !config.refreshToken) { throw new Error("Gmail configuration missing. Please set GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, and GMAIL_REFRESH_TOKEN"); } const oauth2Client = new google.auth.OAuth2( config.clientId, config.clientSecret ); oauth2Client.setCredentials({ refresh_token: config.refreshToken, access_token: config.accessToken, }); return google.gmail({ version: "v1", auth: oauth2Client }); } function parseEmailHeaders(headers: any[]): { from: string; to: string; subject: string; date: string } { const result = { from: "", to: "", subject: "", date: "" }; for (const header of headers) { switch (header.name.toLowerCase()) { case "from": result.from = header.value; break; case "to": result.to = header.value; break; case "subject": result.subject = header.value; break; case "date": result.date = header.value; break; } } return result; } function extractEmailBody(payload: any): string { if (payload.body?.data) { return Buffer.from(payload.body.data, "base64").toString("utf-8"); } if (payload.parts) { for (const part of payload.parts) { if (part.mimeType === "text/plain" && part.body?.data) { return Buffer.from(part.body.data, "base64").toString("utf-8"); } } // If no plain text, try HTML for (const part of payload.parts) { if (part.mimeType === "text/html" && part.body?.data) { return Buffer.from(part.body.data, "base64").toString("utf-8"); } } } return ""; } async function readEmailsViaGmail(args: ReadEmailsArgs): Promise<EmailMessage[]> { const gmail = await getGmailClient(); let query = ""; if (args.folder !== "INBOX") { query += `in:${args.folder}`; } if (args.unreadOnly) { query += query ? " is:unread" : "is:unread"; } const messagesResponse = await gmail.users.messages.list({ userId: "me", q: query || undefined, maxResults: args.limit, }); const messages = messagesResponse.data.messages || []; const emails: EmailMessage[] = []; for (const message of messages) { if (!message.id) continue; const messageDetail = await gmail.users.messages.get({ userId: "me", id: message.id, }); const payload = messageDetail.data.payload; if (!payload?.headers) continue; const headers = parseEmailHeaders(payload.headers); const body = extractEmailBody(payload); const isUnread = messageDetail.data.labelIds?.includes("UNREAD") || false; emails.push({ id: message.id, threadId: messageDetail.data.threadId || "", from: headers.from, to: headers.to, subject: headers.subject, snippet: messageDetail.data.snippet || "", date: headers.date, isUnread, body: body.length > 1000 ? body.substring(0, 1000) + "..." : body, }); } return emails; } export function createReadEmailsTool() { return async (args: ReadEmailsArgs) => { try { const provider = getEmailProvider(); let emails: EmailMessage[] = []; if (provider === "gmail") { emails = await readEmailsViaGmail(args); } else { // Use IMAP for SMTP provider emails = await readEmailsViaImap(args); } const resultText = emails.length > 0 ? `📧 Found ${emails.length} email(s):\n\n` + emails.map((email, index) => `${index + 1}. ${email.isUnread ? "🔵 " : ""}**${email.subject}**\n` + ` From: ${email.from}\n` + ` Date: ${email.date}\n` + ` Snippet: ${email.snippet}\n` + ` Message ID: ${email.id}\n` + (email.body ? ` Body Preview: ${email.body.substring(0, 200)}...\n` : "") + ` ---\n` ).join("\n") : `📭 No emails found in ${args.folder}${args.unreadOnly ? " (unread only)" : ""}`; return { content: [ { type: "text", text: resultText, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [ { type: "text", text: `❌ Failed to read emails: ${errorMessage}`, }, ], }; } }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tianpeijun/email-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server