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;
    }
Behavior1/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Tool has no description.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness1/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Tool has no description.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness1/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Tool has no description.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters1/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Tool has no description.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose1/5

Does the description clearly state what the tool does and how it differs from similar tools?

Tool has no description.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines1/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Tool has no description.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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