Skip to main content
Glama
shuakami

Mail MCP Tool

by shuakami

getAttachment

Retrieve email attachments from a specified folder by providing the email UID and attachment index, then save them to a file for access.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
uidYes
folderNoINBOX
attachmentIndexYes
saveToFileNo

Implementation Reference

  • Primary handler function that implements the core logic for retrieving email attachments via IMAP. It fetches the email structure, identifies the specified attachment, and downloads its content.
    async getAttachment(uid: number, folder: string = 'INBOX', attachmentIndex: number): Promise<{ filename: string; content: Buffer; contentType: string } | null> {
      await this.connectImap();
      console.log(`正在获取UID ${uid} 的第 ${attachmentIndex} 个附件...`);
    
      return new Promise((resolve, reject) => {
        this.imapClient.openBox(folder, true, (err) => {
          if (err) {
            console.error(`打开文件夹 ${folder} 失败:`, err);
            reject(err);
            return;
          }
    
          const f = this.imapClient.fetch(`${uid}`, { bodies: '', struct: true });
          
          let attachmentInfo: { partID: string; filename: string; contentType: string } | null = null;
          
          f.on('message', (msg, seqno) => {
            msg.on('body', (stream, info) => {
              // 这个事件处理器只是为了确保消息体被处理
              stream.on('data', () => {});
              stream.on('end', () => {});
            });
    
            msg.once('attributes', (attrs) => {
              try {
                const struct = attrs.struct;
                const attachments = this.findAttachmentParts(struct);
                
                if (attachments.length <= attachmentIndex) {
                  console.log(`附件索引 ${attachmentIndex} 超出范围,附件总数: ${attachments.length}`);
                  resolve(null);
                  return;
                }
                
                attachmentInfo = attachments[attachmentIndex];
                console.log(`找到附件信息:`, attachmentInfo);
              } catch (error) {
                console.error(`解析附件结构时出错:`, error);
                reject(error);
              }
            });
            
            msg.once('end', () => {
              if (!attachmentInfo) {
                console.log(`未找到附件或附件索引无效`);
                resolve(null);
                return;
              }
              
              // 获取附件内容
              const attachmentFetch = this.imapClient.fetch(`${uid}`, { 
                bodies: [attachmentInfo.partID],
                struct: true 
              });
              
              let buffer = Buffer.alloc(0);
              
              attachmentFetch.on('message', (msg, seqno) => {
                msg.on('body', (stream, info) => {
                  stream.on('data', (chunk) => {
                    buffer = Buffer.concat([buffer, chunk]);
                  });
                  
                  stream.once('end', () => {
                    console.log(`附件内容下载完成,大小: ${buffer.length} 字节`);
                  });
                });
                
                msg.once('end', () => {
                  console.log(`附件消息处理完成`);
                });
              });
              
              attachmentFetch.once('error', (err) => {
                console.error(`获取附件内容时出错:`, err);
                reject(err);
              });
              
              attachmentFetch.once('end', () => {
                console.log(`附件获取流程结束`);
                resolve({
                  filename: attachmentInfo!.filename,
                  content: buffer,
                  contentType: attachmentInfo!.contentType
                });
              });
            });
          });
          
          f.once('error', (err) => {
            console.error(`获取邮件时出错:`, err);
            reject(err);
          });
          
          f.once('end', () => {
            if (!attachmentInfo) {
              console.log(`未找到附件或结构中没有附件`);
              resolve(null);
            }
          });
        });
      });
    }
  • MCP tool registration for 'getAttachment' including input schema validation with Zod and wrapper handler that invokes the MailService method and handles response (saving to file or displaying).
    this.server.tool(
      "getAttachment",
      {
        uid: z.number(),
        folder: z.string().default('INBOX'),
        attachmentIndex: z.number(),
        saveToFile: z.boolean().default(true)
      },
      async (params) => {
        try {
          const attachment = await this.mailService.getAttachment(
            params.uid, 
            params.folder, 
            params.attachmentIndex
          );
          
          if (!attachment) {
            return {
              content: [
                { type: "text", text: `未找到UID为${params.uid}的邮件的第${params.attachmentIndex}个附件` }
              ]
            };
          }
          
          // 根据是否保存到文件处理附件
          if (params.saveToFile) {
            // 创建附件保存目录
            const downloadDir = path.join(process.cwd(), 'downloads');
            if (!fs.existsSync(downloadDir)) {
              fs.mkdirSync(downloadDir, { recursive: true });
            }
            
            // 生成安全的文件名(去除非法字符)
            const safeFilename = attachment.filename.replace(/[/\\?%*:|"<>]/g, '-');
            const filePath = path.join(downloadDir, safeFilename);
            
            // 写入文件
            fs.writeFileSync(filePath, attachment.content);
            
            return {
              content: [
                { 
                  type: "text", 
                  text: `附件 "${attachment.filename}" 已下载保存至 ${filePath}\n类型: ${attachment.contentType}\n大小: ${Math.round(attachment.content.length / 1024)} KB` 
                }
              ]
            };
          } else {
            // 根据内容类型处理内容
            if (attachment.contentType.startsWith('text/') || 
                attachment.contentType === 'application/json') {
              // 文本文件显示内容
              const textContent = attachment.content.toString('utf-8');
              return {
                content: [
                  { 
                    type: "text", 
                    text: `📎 附件 "${attachment.filename}" (${attachment.contentType})\n\n${textContent.substring(0, 10000)}${textContent.length > 10000 ? '\n\n[内容过长,已截断]' : ''}` 
                  }
                ]
              };
            } else if (attachment.contentType.startsWith('image/')) {
              // 图片文件提供Base64编码
              const base64Content = attachment.content.toString('base64');
              return {
                content: [
                  { 
                    type: "text", 
                    text: `📎 图片附件 "${attachment.filename}" (${attachment.contentType})\n大小: ${Math.round(attachment.content.length / 1024)} KB\n\n[图片内容已转为Base64编码,可用于在线预览]` 
                  }
                ]
              };
            } else {
              // 其他二进制文件
              return {
                content: [
                  { 
                    type: "text", 
                    text: `📎 二进制附件 "${attachment.filename}" (${attachment.contentType})\n大小: ${Math.round(attachment.content.length / 1024)} KB\n\n[二进制内容无法直接显示]` 
                  }
                ]
              };
            }
          }
        } catch (error) {
          return {
            content: [
              { type: "text", text: `获取附件时发生错误: ${error instanceof Error ? error.message : String(error)}` }
            ]
          };
        }
      }
    );
  • Input schema definition using Zod for the getAttachment tool parameters: uid (required number), folder (string, default 'INBOX'), attachmentIndex (required number), saveToFile (boolean, default true).
      uid: z.number(),
      folder: z.string().default('INBOX'),
      attachmentIndex: z.number(),
      saveToFile: z.boolean().default(true)
    },
  • Helper method used by getAttachment to parse email MIME structure and find all attachment parts with their identifiers and metadata.
    private findAttachmentParts(struct: any[], prefix = ''): { partID: string; filename: string; contentType: string }[] {
      const attachments: { partID: string; filename: string; contentType: string }[] = [];
      
      if (!struct || !Array.isArray(struct)) return attachments;
      
      const processStruct = (s: any, partID = '') => {
        if (Array.isArray(s)) {
          // 多部分结构
          if (s[0] && typeof s[0] === 'object' && s[0].partID) {
            // 这是一个具体的部分
            if (s[0].disposition && 
                (s[0].disposition.type.toLowerCase() === 'attachment' || 
                 s[0].disposition.type.toLowerCase() === 'inline')) {
              let filename = '';
              if (s[0].disposition.params && s[0].disposition.params.filename) {
                filename = s[0].disposition.params.filename;
              } else if (s[0].params && s[0].params.name) {
                filename = s[0].params.name;
              }
              
              const contentType = s[0].type + '/' + s[0].subtype;
              
              if (filename) {
                attachments.push({
                  partID: s[0].partID,
                  filename: filename,
                  contentType: contentType
                });
              }
            }
          } else {
            // 遍历数组中的每个元素
            for (let i = 0; i < s.length; i++) {
              const newPrefix = partID ? `${partID}.${i + 1}` : `${i + 1}`;
              if (Array.isArray(s[i])) {
                processStruct(s[i], newPrefix);
              } else if (typeof s[i] === 'object') {
                // 可能是一个部分定义
                if (s[i].disposition && 
                    (s[i].disposition.type.toLowerCase() === 'attachment' || 
                     s[i].disposition.type.toLowerCase() === 'inline')) {
                  let filename = '';
                  if (s[i].disposition.params && s[i].disposition.params.filename) {
                    filename = s[i].disposition.params.filename;
                  } else if (s[i].params && s[i].params.name) {
                    filename = s[i].params.name;
                  }
                  
                  const contentType = s[i].type + '/' + s[i].subtype;
                  
                  if (filename) {
                    attachments.push({
                      partID: newPrefix,
                      filename: filename,
                      contentType: contentType
                    });
                  }
                }
              }
            }
          }
        }
      };
      
      processStruct(struct, prefix);
      return attachments;
    }

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/shuakami/mcp-mail'

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