@lengelhard/imap-email-mcp 1.0.1 → 1.0.3
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 +119 -5
- 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, partID: node.partID });
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
@@ -347,6 +347,33 @@ 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
|
+
},
|
|
350
377
|
{
|
|
351
378
|
name: 'delete_email',
|
|
352
379
|
description: 'Delete an email by UID',
|
|
@@ -515,7 +542,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
515
542
|
const fromParser = new Map(
|
|
516
543
|
(parsed.attachments || []).map(a => [
|
|
517
544
|
(a.filename || '').toLowerCase(),
|
|
518
|
-
{ filename: a.filename, contentType: a.contentType, size: a.size }
|
|
545
|
+
{ filename: a.filename, contentType: a.contentType, size: a.size, partID: null }
|
|
519
546
|
])
|
|
520
547
|
);
|
|
521
548
|
|
|
@@ -523,10 +550,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
523
550
|
// may have skipped: inline parts with filenames and parts that have
|
|
524
551
|
// no Content-Disposition at all but carry a Content-Type "name" param.
|
|
525
552
|
const fromStruct = findAttachmentParts(msg.attributes.struct || []);
|
|
526
|
-
for (const { filename, contentType } of fromStruct) {
|
|
553
|
+
for (const { filename, contentType, partID } of fromStruct) {
|
|
527
554
|
const key = (filename || '').toLowerCase();
|
|
528
|
-
if (filename
|
|
529
|
-
fromParser.
|
|
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
|
+
}
|
|
530
562
|
}
|
|
531
563
|
}
|
|
532
564
|
|
|
@@ -771,6 +803,88 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
771
803
|
};
|
|
772
804
|
}
|
|
773
805
|
|
|
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
|
+
|
|
774
888
|
case 'delete_email': {
|
|
775
889
|
const folder = args.folder || 'INBOX';
|
|
776
890
|
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.3",
|
|
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",
|