@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.
- package/index.js +66 -120
- 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
|
|
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
|
|
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
|
|
526
|
+
for (const { filename, contentType } of fromStruct) {
|
|
554
527
|
const key = (filename || '').toLowerCase();
|
|
555
|
-
if (filename) {
|
|
556
|
-
|
|
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
|
+
"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",
|