@lengelhard/imap-email-mcp 1.0.3 → 1.0.5

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 +66 -120
  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
 
@@ -347,33 +347,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
347
347
  required: ['to', 'subject']
348
348
  }
349
349
  },
350
- {
351
- name: 'download_attachment',
352
- description: 'Download a specific email attachment as base64-encoded content. Use the partID from get_email response.',
353
- inputSchema: {
354
- type: 'object',
355
- properties: {
356
- uid: {
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)'
363
- },
364
- filename: {
365
- type: 'string',
366
- description: 'Filename of the attachment (used as fallback to find part_id if not provided)'
367
- },
368
- folder: {
369
- type: 'string',
370
- description: 'Folder name (default: INBOX)',
371
- default: 'INBOX'
372
- }
373
- },
374
- required: ['uid']
375
- }
376
- },
377
350
  {
378
351
  name: 'delete_email',
379
352
  description: 'Delete an email by UID',
@@ -542,7 +515,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
542
515
  const fromParser = new Map(
543
516
  (parsed.attachments || []).map(a => [
544
517
  (a.filename || '').toLowerCase(),
545
- { filename: a.filename, contentType: a.contentType, size: a.size, partID: null }
518
+ { filename: a.filename, contentType: a.contentType, size: a.size }
546
519
  ])
547
520
  );
548
521
 
@@ -550,15 +523,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
550
523
  // may have skipped: inline parts with filenames and parts that have
551
524
  // no Content-Disposition at all but carry a Content-Type "name" param.
552
525
  const fromStruct = findAttachmentParts(msg.attributes.struct || []);
553
- for (const { filename, contentType, partID } of fromStruct) {
526
+ for (const { filename, contentType } of fromStruct) {
554
527
  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
- }
528
+ if (filename && !fromParser.has(key)) {
529
+ fromParser.set(key, { filename, contentType, size: null });
562
530
  }
563
531
  }
564
532
 
@@ -789,102 +757,80 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
789
757
  bcc: args.bcc
790
758
  };
791
759
 
760
+ // Send via SMTP and capture the raw RFC 5322 message
792
761
  const info = await transporter.sendMail(mailOptions);
793
762
 
763
+ // Build RFC 5322 message string to APPEND to Sent folder via IMAP
764
+ // Use the same content that was sent so the Sent copy is identical
765
+ const sentDate = new Date();
766
+ const boundary = `----=_Part_${Date.now()}`;
767
+ let rawMessage = '';
768
+ rawMessage += `From: ${SMTP_CONFIG.auth.user}\r\n`;
769
+ rawMessage += `To: ${args.to}\r\n`;
770
+ if (args.cc) rawMessage += `Cc: ${args.cc}\r\n`;
771
+ if (args.bcc) rawMessage += `Bcc: ${args.bcc}\r\n`;
772
+ rawMessage += `Subject: ${args.subject}\r\n`;
773
+ rawMessage += `Date: ${sentDate.toUTCString()}\r\n`;
774
+ rawMessage += `Message-ID: ${info.messageId}\r\n`;
775
+ rawMessage += `MIME-Version: 1.0\r\n`;
776
+
777
+ if (args.html) {
778
+ rawMessage += `Content-Type: multipart/alternative; boundary="${boundary}"\r\n\r\n`;
779
+ rawMessage += `--${boundary}\r\n`;
780
+ rawMessage += `Content-Type: text/plain; charset=utf-8\r\n\r\n`;
781
+ rawMessage += `${args.body || ''}\r\n`;
782
+ rawMessage += `--${boundary}\r\n`;
783
+ rawMessage += `Content-Type: text/html; charset=utf-8\r\n\r\n`;
784
+ rawMessage += `${args.html}\r\n`;
785
+ rawMessage += `--${boundary}--\r\n`;
786
+ } else {
787
+ rawMessage += `Content-Type: text/plain; charset=utf-8\r\n\r\n`;
788
+ rawMessage += `${args.body || ''}\r\n`;
789
+ }
790
+
791
+ // APPEND to Sent folder via IMAP
792
+ let sentFolderResult = 'skipped';
793
+ try {
794
+ const connection = await connectIMAP();
795
+ try {
796
+ // Try common Sent folder names in order
797
+ const sentFolderCandidates = ['Sent', 'INBOX.Sent', 'Sent Items', 'Sent Mail', '[Gmail]/Sent Mail'];
798
+ let sentFolder = 'Sent'; // default fallback
799
+
800
+ const folders = await connection.getBoxes();
801
+ const flatFolders = Object.keys(folders);
802
+ for (const candidate of sentFolderCandidates) {
803
+ if (flatFolders.some(f => f.toLowerCase() === candidate.toLowerCase())) {
804
+ sentFolder = candidate;
805
+ break;
806
+ }
807
+ }
808
+
809
+ await connection.append(rawMessage, {
810
+ mailbox: sentFolder,
811
+ flags: ['\\Seen']
812
+ });
813
+ sentFolderResult = `saved to ${sentFolder}`;
814
+ } finally {
815
+ connection.end();
816
+ }
817
+ } catch (appendErr) {
818
+ sentFolderResult = `failed: ${appendErr.message}`;
819
+ }
820
+
794
821
  return {
795
822
  content: [{
796
823
  type: 'text',
797
824
  text: JSON.stringify({
798
825
  success: true,
799
826
  messageId: info.messageId,
800
- response: info.response
827
+ response: info.response,
828
+ sentFolder: sentFolderResult
801
829
  }, null, 2)
802
830
  }]
803
831
  };
804
832
  }
805
833
 
806
- case 'download_attachment': {
807
- const folder = args.folder || 'INBOX';
808
- const connection = await connectIMAP();
809
-
810
- 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
817
- });
818
-
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);
869
-
870
- return {
871
- content: [{
872
- type: 'text',
873
- 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
880
- }, null, 2)
881
- }]
882
- };
883
- } finally {
884
- connection.end();
885
- }
886
- }
887
-
888
834
  case 'delete_email': {
889
835
  const folder = args.folder || 'INBOX';
890
836
  const connection = await connectIMAP();
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.5",
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",