@lengelhard/imap-email-mcp 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +31 -86
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -83,7 +83,7 @@ function findAttachmentParts(struct, parts = []) {
83
83
  );
84
84
 
85
85
  if (isAttachment) {
86
- parts.push({ node, filename, contentType, disposition, partID: node.partID });
86
+ parts.push({ node, filename, contentType, disposition });
87
87
  }
88
88
  }
89
89
 
@@ -348,27 +348,24 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
348
348
  }
349
349
  },
350
350
  {
351
- name: 'download_attachment',
352
- description: 'Download a specific email attachment as base64-encoded content. Use the partID from get_email response.',
351
+ name: 'move_email',
352
+ description: 'Move an email from one folder to another (e.g. INBOX to Archive)',
353
353
  inputSchema: {
354
354
  type: 'object',
355
355
  properties: {
356
356
  uid: {
357
357
  type: 'number',
358
- description: 'Email UID'
359
- },
360
- part_id: {
361
- type: 'string',
362
- description: 'MIME part ID of the attachment (from get_email response)'
358
+ description: 'Email UID to move'
363
359
  },
364
- filename: {
360
+ from_folder: {
365
361
  type: 'string',
366
- description: 'Filename of the attachment (used as fallback to find part_id if not provided)'
362
+ description: 'Source folder (default: INBOX)',
363
+ default: 'INBOX'
367
364
  },
368
- folder: {
365
+ to_folder: {
369
366
  type: 'string',
370
- description: 'Folder name (default: INBOX)',
371
- default: 'INBOX'
367
+ description: 'Destination folder (e.g. Archive, Sent, Trash)',
368
+ default: 'Archive'
372
369
  }
373
370
  },
374
371
  required: ['uid']
@@ -542,7 +539,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
542
539
  const fromParser = new Map(
543
540
  (parsed.attachments || []).map(a => [
544
541
  (a.filename || '').toLowerCase(),
545
- { filename: a.filename, contentType: a.contentType, size: a.size, partID: null }
542
+ { filename: a.filename, contentType: a.contentType, size: a.size }
546
543
  ])
547
544
  );
548
545
 
@@ -550,15 +547,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
550
547
  // may have skipped: inline parts with filenames and parts that have
551
548
  // no Content-Disposition at all but carry a Content-Type "name" param.
552
549
  const fromStruct = findAttachmentParts(msg.attributes.struct || []);
553
- for (const { filename, contentType, partID } of fromStruct) {
550
+ for (const { filename, contentType } of fromStruct) {
554
551
  const key = (filename || '').toLowerCase();
555
- if (filename) {
556
- if (!fromParser.has(key)) {
557
- fromParser.set(key, { filename, contentType, size: null, partID });
558
- } else {
559
- // Enrich existing entry with partID from struct
560
- fromParser.get(key).partID = partID;
561
- }
552
+ if (filename && !fromParser.has(key)) {
553
+ fromParser.set(key, { filename, contentType, size: null });
562
554
  }
563
555
  }
564
556
 
@@ -803,80 +795,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
803
795
  };
804
796
  }
805
797
 
806
- case 'download_attachment': {
807
- const folder = args.folder || 'INBOX';
798
+ case 'move_email': {
799
+ const fromFolder = args.from_folder || 'INBOX';
800
+ const toFolder = args.to_folder || 'Archive';
808
801
  const connection = await connectIMAP();
809
802
 
810
803
  try {
811
- await connection.openBox(folder);
812
-
813
- // First fetch the struct to find the part if part_id not provided
814
- const structFetch = await connection.search([['UID', args.uid]], {
815
- bodies: [],
816
- struct: true
804
+ await connection.openBox(fromFolder);
805
+
806
+ // imap-simple exposes the underlying imap connection via ._imap
807
+ // Use it to copy the message then delete the original
808
+ await new Promise((resolve, reject) => {
809
+ connection.imap.copy(args.uid, toFolder, (err) => {
810
+ if (err) reject(err);
811
+ else resolve();
812
+ });
817
813
  });
818
814
 
819
- if (structFetch.length === 0) {
820
- return { content: [{ type: 'text', text: 'Email not found' }] };
821
- }
822
-
823
- const msg = structFetch[0];
824
- let partID = args.part_id;
825
-
826
- // If no partID given, find it by filename
827
- if (!partID && args.filename) {
828
- const attachmentParts = findAttachmentParts(msg.attributes.struct || []);
829
- const match = attachmentParts.find(p =>
830
- (p.filename || '').toLowerCase() === args.filename.toLowerCase()
831
- );
832
- if (match) partID = match.partID;
833
- }
834
-
835
- if (!partID) {
836
- return {
837
- content: [{ type: 'text', text: 'Could not determine part ID. Please provide part_id from get_email response.' }],
838
- isError: true
839
- };
840
- }
841
-
842
- // Fetch the specific MIME part
843
- const partFetch = await connection.search([['UID', args.uid]], {
844
- bodies: [partID],
845
- struct: true
846
- });
847
-
848
- if (partFetch.length === 0) {
849
- return { content: [{ type: 'text', text: 'Attachment part not found' }] };
850
- }
851
-
852
- const partMsg = partFetch[0];
853
- const partData = partMsg.parts.find(p => p.which === partID);
854
-
855
- if (!partData) {
856
- return { content: [{ type: 'text', text: `Part ${partID} not found in message` }] };
857
- }
858
-
859
- // IMAP attachment parts are already base64-encoded by the mail server.
860
- // Return the raw content as-is — do NOT re-encode, which would double-encode.
861
- const rawContent = partData.body;
862
- const base64Content = Buffer.isBuffer(rawContent)
863
- ? rawContent.toString('ascii').replace(/\s/g, '') // buffer → clean base64 string
864
- : rawContent.replace(/\s/g, ''); // string → strip whitespace/newlines
865
-
866
- // Get filename from struct for reference
867
- const attachmentParts = findAttachmentParts(msg.attributes.struct || []);
868
- const attachmentInfo = attachmentParts.find(p => p.partID === partID);
815
+ // Mark original as deleted and expunge
816
+ await connection.addFlags(args.uid, ['\\Deleted']);
817
+ await connection.closeBox(true); // true = expunge on close
869
818
 
870
819
  return {
871
820
  content: [{
872
821
  type: 'text',
873
822
  text: JSON.stringify({
874
- uid: args.uid,
875
- partID,
876
- filename: attachmentInfo?.filename || args.filename || 'attachment',
877
- contentType: attachmentInfo?.contentType || 'application/octet-stream',
878
- encoding: 'base64',
879
- data: base64Content
823
+ success: true,
824
+ message: `Email ${args.uid} moved from ${fromFolder} to ${toFolder}`
880
825
  }, null, 2)
881
826
  }]
882
827
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lengelhard/imap-email-mcp",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "MCP server for Claude Code that provides email capabilities through IMAP/SMTP. Read, search, compose, and manage emails from any IMAP provider.",
5
5
  "type": "module",
6
6
  "main": "index.js",