Skip to main content
Glama
replyEmail.ts8.95 kB
import { google } from "googleapis"; import nodemailer from "nodemailer"; import ImapOriginal from "imap"; // @ts-ignore import ImapMkl from "imap-mkl"; import { simpleParser } from "mailparser"; import { getEmailAccounts, getDefaultAccount } from "../utils/emailAccounts.js"; interface ReplyEmailArgs { messageId: string; body: string; replyAll: boolean; html: boolean; } function getEmailProvider(): string { return process.env.EMAIL_PROVIDER || "smtp"; } function getSmtpConfig() { return { host: process.env.SMTP_HOST!, port: parseInt(process.env.SMTP_PORT || "587"), secure: process.env.SMTP_SECURE === "true", auth: { user: process.env.SMTP_USER!, pass: process.env.SMTP_PASS!, }, }; } function getImapConfig() { const accounts = getEmailAccounts(); 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 getSmtpConfigForAccount() { const accounts = getEmailAccounts(); const defaultAccountName = getDefaultAccount(); const defaultAccount = accounts.get(defaultAccountName); if (defaultAccount) { return { host: defaultAccount.smtp.host, port: defaultAccount.smtp.port, secure: defaultAccount.smtp.secure, auth: { user: defaultAccount.smtp.user, pass: defaultAccount.smtp.pass, }, }; } return getSmtpConfig(); } async function getOriginalEmailViaImap(messageId: string): Promise<any> { return new Promise((resolve, reject) => { const config = getImapConfig(); // 检查是否是 163 邮箱 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); } imap.once("ready", () => { imap.openBox("INBOX", true, (err: any, box: any) => { if (err) { imap.end(); return reject(err); } // Convert messageId to sequence number const seqno = parseInt(messageId); if (isNaN(seqno)) { imap.end(); return reject(new Error("Invalid message ID")); } const fetch = imap.fetch([seqno], { bodies: "", struct: true, }); fetch.on("message", (msg: any, seqno: number) => { msg.on("body", (stream: any) => { simpleParser(stream, async (err: any, parsed: any) => { if (err) { imap.end(); return reject(err); } imap.end(); resolve({ from: parsed.from?.text || "", to: parsed.to?.text || "", cc: parsed.cc?.text || "", subject: parsed.subject || "", messageId: parsed.messageId || "", references: parsed.references || [], }); }); }); }); fetch.once("error", (err: any) => { imap.end(); reject(err); }); }); }); imap.once("error", (err: any) => { reject(err); }); imap.connect(); }); } async function replyViaSmtp(args: ReplyEmailArgs, originalEmail: any): Promise<string> { const config = getSmtpConfigForAccount(); const transporter = nodemailer.createTransport(config); const replySubject = originalEmail.subject.startsWith("Re:") ? originalEmail.subject : `Re: ${originalEmail.subject}`; let toAddresses = originalEmail.from; if (args.replyAll) { const allRecipients = [originalEmail.from, originalEmail.to, originalEmail.cc] .filter(addr => addr) .join(", "); toAddresses = allRecipients; } const mailOptions = { from: process.env.DEFAULT_FROM_EMAIL || process.env.SMTP_USER, to: toAddresses, subject: replySubject, text: args.html ? undefined : args.body, html: args.html ? args.body : undefined, inReplyTo: originalEmail.messageId, references: [...(originalEmail.references || []), originalEmail.messageId].filter(Boolean), }; const info = await transporter.sendMail(mailOptions); return info.messageId; } async function replyViaGmail(args: ReplyEmailArgs): Promise<any> { const config = { clientId: process.env.GMAIL_CLIENT_ID!, clientSecret: process.env.GMAIL_CLIENT_SECRET!, refreshToken: process.env.GMAIL_REFRESH_TOKEN!, accessToken: process.env.GMAIL_ACCESS_TOKEN, }; 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, }); const gmail = google.gmail({ version: "v1", auth: oauth2Client }); const originalMessage = await gmail.users.messages.get({ userId: "me", id: args.messageId, }); const payload = originalMessage.data.payload; if (!payload?.headers) { throw new Error("Could not find original message headers"); } const headers = payload.headers.reduce((acc: any, header: any) => { acc[header.name.toLowerCase()] = header.value; return acc; }, {}); const originalSubject = headers.subject || ""; const replySubject = originalSubject.startsWith("Re:") ? originalSubject : `Re: ${originalSubject}`; const originalFrom = headers.from || ""; let toAddresses = originalFrom; if (args.replyAll) { const originalTo = headers.to || ""; const originalCc = headers.cc || ""; toAddresses = [originalFrom, originalTo, originalCc].filter(addr => addr).join(", "); } const emailLines = [ `To: ${toAddresses}`, `Subject: ${replySubject}`, `In-Reply-To: ${headers["message-id"] || ""}`, `References: ${headers.references || headers["message-id"] || ""}`, `Content-Type: ${args.html ? "text/html" : "text/plain"}; charset=utf-8`, "", args.body, ]; const email = emailLines.join("\n"); const encodedEmail = Buffer.from(email).toString("base64").replace(/\+/g, "-").replace(/\//g, "_"); const result = await gmail.users.messages.send({ userId: "me", requestBody: { raw: encodedEmail, threadId: originalMessage.data.threadId, }, }); return { toAddresses, replySubject, messageId: result.data.id, }; } export function createReplyEmailTool() { return async (args: ReplyEmailArgs) => { try { const provider = getEmailProvider(); let result: any; if (provider === "gmail") { result = await replyViaGmail(args); } else { // Use SMTP for reply const originalEmail = await getOriginalEmailViaImap(args.messageId); const messageId = await replyViaSmtp(args, originalEmail); const replySubject = originalEmail.subject.startsWith("Re:") ? originalEmail.subject : `Re: ${originalEmail.subject}`; let toAddresses = originalEmail.from; if (args.replyAll) { toAddresses = [originalEmail.from, originalEmail.to, originalEmail.cc] .filter(addr => addr) .join(", "); } result = { toAddresses, replySubject, messageId, }; } return { content: [ { type: "text", text: `✅ 回复邮件发送成功!\n\n详情:\n- 收件人: ${result.toAddresses}\n- 主题: ${result.replySubject}\n- 消息ID: ${result.messageId}\n- 回复全部: ${args.replyAll ? "是" : "否"}\n- 格式: ${args.html ? "HTML" : "纯文本"}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [ { type: "text", text: `❌ 回复邮件失败: ${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