@lengelhard/imap-email-mcp 1.0.5 → 1.2.0
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 +264 -62
- 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',
|
|
@@ -365,6 +392,75 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
365
392
|
},
|
|
366
393
|
required: ['uid']
|
|
367
394
|
}
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
name: 'move_email',
|
|
398
|
+
description: 'Move an email from one folder to another (e.g. Inbox → Action, Waiting, Delegated, Hold)',
|
|
399
|
+
inputSchema: {
|
|
400
|
+
type: 'object',
|
|
401
|
+
properties: {
|
|
402
|
+
uid: {
|
|
403
|
+
type: 'number',
|
|
404
|
+
description: 'Email UID to move'
|
|
405
|
+
},
|
|
406
|
+
from_folder: {
|
|
407
|
+
type: 'string',
|
|
408
|
+
description: 'Source folder name (default: INBOX)',
|
|
409
|
+
default: 'INBOX'
|
|
410
|
+
},
|
|
411
|
+
to_folder: {
|
|
412
|
+
type: 'string',
|
|
413
|
+
description: 'Destination folder name (e.g. Action, Waiting, Delegated, Hold, Archive)'
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
required: ['uid', 'to_folder']
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
name: 'flag_email',
|
|
421
|
+
description: 'Add or remove the \\Flagged flag on an email (star/unstar). Used to manage the /Flagged urgency overlay.',
|
|
422
|
+
inputSchema: {
|
|
423
|
+
type: 'object',
|
|
424
|
+
properties: {
|
|
425
|
+
uid: {
|
|
426
|
+
type: 'number',
|
|
427
|
+
description: 'Email UID'
|
|
428
|
+
},
|
|
429
|
+
folder: {
|
|
430
|
+
type: 'string',
|
|
431
|
+
description: 'Folder containing the email (default: INBOX)',
|
|
432
|
+
default: 'INBOX'
|
|
433
|
+
},
|
|
434
|
+
flagged: {
|
|
435
|
+
type: 'boolean',
|
|
436
|
+
description: 'true to add the flag (star), false to remove it (unstar)'
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
required: ['uid', 'flagged']
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
name: 'mark_seen',
|
|
444
|
+
description: 'Mark an email as read (seen) or unread (unseen). Used to signal triage state via the \\Seen flag.',
|
|
445
|
+
inputSchema: {
|
|
446
|
+
type: 'object',
|
|
447
|
+
properties: {
|
|
448
|
+
uid: {
|
|
449
|
+
type: 'number',
|
|
450
|
+
description: 'Email UID'
|
|
451
|
+
},
|
|
452
|
+
folder: {
|
|
453
|
+
type: 'string',
|
|
454
|
+
description: 'Folder containing the email (default: INBOX)',
|
|
455
|
+
default: 'INBOX'
|
|
456
|
+
},
|
|
457
|
+
seen: {
|
|
458
|
+
type: 'boolean',
|
|
459
|
+
description: 'true to mark as read, false to mark as unread'
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
required: ['uid', 'seen']
|
|
463
|
+
}
|
|
368
464
|
}
|
|
369
465
|
]
|
|
370
466
|
};
|
|
@@ -757,80 +853,97 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
757
853
|
bcc: args.bcc
|
|
758
854
|
};
|
|
759
855
|
|
|
760
|
-
// Send via SMTP and capture the raw RFC 5322 message
|
|
761
856
|
const info = await transporter.sendMail(mailOptions);
|
|
762
857
|
|
|
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
|
-
|
|
821
858
|
return {
|
|
822
859
|
content: [{
|
|
823
860
|
type: 'text',
|
|
824
861
|
text: JSON.stringify({
|
|
825
862
|
success: true,
|
|
826
863
|
messageId: info.messageId,
|
|
827
|
-
response: info.response
|
|
828
|
-
sentFolder: sentFolderResult
|
|
864
|
+
response: info.response
|
|
829
865
|
}, null, 2)
|
|
830
866
|
}]
|
|
831
867
|
};
|
|
832
868
|
}
|
|
833
869
|
|
|
870
|
+
case 'download_attachment': {
|
|
871
|
+
const folder = args.folder || 'INBOX';
|
|
872
|
+
const connection = await connectIMAP();
|
|
873
|
+
|
|
874
|
+
try {
|
|
875
|
+
await connection.openBox(folder);
|
|
876
|
+
|
|
877
|
+
// Fetch struct to locate the part
|
|
878
|
+
const structFetch = await connection.search([['UID', args.uid]], {
|
|
879
|
+
bodies: [],
|
|
880
|
+
struct: true
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
if (structFetch.length === 0) {
|
|
884
|
+
return { content: [{ type: 'text', text: 'Email not found' }] };
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const msg = structFetch[0];
|
|
888
|
+
let partID = args.part_id;
|
|
889
|
+
|
|
890
|
+
// If no partID given, find it by filename
|
|
891
|
+
if (!partID && args.filename) {
|
|
892
|
+
const attachmentParts = findAttachmentParts(msg.attributes.struct || []);
|
|
893
|
+
const match = attachmentParts.find(p =>
|
|
894
|
+
(p.filename || '').toLowerCase() === args.filename.toLowerCase()
|
|
895
|
+
);
|
|
896
|
+
if (match) partID = match.partID;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (!partID) {
|
|
900
|
+
return {
|
|
901
|
+
content: [{ type: 'text', text: 'Could not determine part ID. Please provide part_id from get_email response.' }],
|
|
902
|
+
isError: true
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Fetch the specific MIME part
|
|
907
|
+
const partFetch = await connection.search([['UID', args.uid]], {
|
|
908
|
+
bodies: [partID],
|
|
909
|
+
struct: true
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
if (partFetch.length === 0) {
|
|
913
|
+
return { content: [{ type: 'text', text: 'Attachment part not found' }] };
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const partData = partFetch[0].parts.find(p => p.which === partID);
|
|
917
|
+
|
|
918
|
+
if (!partData) {
|
|
919
|
+
return { content: [{ type: 'text', text: `Part ${partID} not found in message` }] };
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// imap-simple returns the body already base64-encoded by the mail server.
|
|
923
|
+
// Re-encoding would corrupt the output — strip whitespace and return directly.
|
|
924
|
+
const base64Content = String(partData.body).replace(/\s+/g, '');
|
|
925
|
+
|
|
926
|
+
const attachmentParts = findAttachmentParts(msg.attributes.struct || []);
|
|
927
|
+
const attachmentInfo = attachmentParts.find(p => p.partID === partID);
|
|
928
|
+
|
|
929
|
+
return {
|
|
930
|
+
content: [{
|
|
931
|
+
type: 'text',
|
|
932
|
+
text: JSON.stringify({
|
|
933
|
+
uid: args.uid,
|
|
934
|
+
partID,
|
|
935
|
+
filename: attachmentInfo?.filename || args.filename || 'attachment',
|
|
936
|
+
contentType: attachmentInfo?.contentType || 'application/octet-stream',
|
|
937
|
+
encoding: 'base64',
|
|
938
|
+
data: base64Content
|
|
939
|
+
}, null, 2)
|
|
940
|
+
}]
|
|
941
|
+
};
|
|
942
|
+
} finally {
|
|
943
|
+
connection.end();
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
834
947
|
case 'delete_email': {
|
|
835
948
|
const folder = args.folder || 'INBOX';
|
|
836
949
|
const connection = await connectIMAP();
|
|
@@ -846,6 +959,95 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
846
959
|
}
|
|
847
960
|
}
|
|
848
961
|
|
|
962
|
+
case 'move_email': {
|
|
963
|
+
const fromFolder = args.from_folder || 'INBOX';
|
|
964
|
+
const toFolder = args.to_folder;
|
|
965
|
+
const connection = await connectIMAP();
|
|
966
|
+
|
|
967
|
+
try {
|
|
968
|
+
await connection.openBox(fromFolder);
|
|
969
|
+
|
|
970
|
+
await new Promise((resolve, reject) => {
|
|
971
|
+
connection.imap.move(args.uid, toFolder, (err) => {
|
|
972
|
+
if (err) reject(err);
|
|
973
|
+
else resolve();
|
|
974
|
+
});
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
return {
|
|
978
|
+
content: [{
|
|
979
|
+
type: 'text',
|
|
980
|
+
text: JSON.stringify({
|
|
981
|
+
success: true,
|
|
982
|
+
uid: args.uid,
|
|
983
|
+
from: fromFolder,
|
|
984
|
+
to: toFolder
|
|
985
|
+
}, null, 2)
|
|
986
|
+
}]
|
|
987
|
+
};
|
|
988
|
+
} finally {
|
|
989
|
+
connection.end();
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
case 'flag_email': {
|
|
994
|
+
const folder = args.folder || 'INBOX';
|
|
995
|
+
const connection = await connectIMAP();
|
|
996
|
+
|
|
997
|
+
try {
|
|
998
|
+
await connection.openBox(folder);
|
|
999
|
+
|
|
1000
|
+
if (args.flagged) {
|
|
1001
|
+
await connection.addFlags(args.uid, ['\\Flagged']);
|
|
1002
|
+
} else {
|
|
1003
|
+
await connection.delFlags(args.uid, ['\\Flagged']);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
return {
|
|
1007
|
+
content: [{
|
|
1008
|
+
type: 'text',
|
|
1009
|
+
text: JSON.stringify({
|
|
1010
|
+
success: true,
|
|
1011
|
+
uid: args.uid,
|
|
1012
|
+
folder,
|
|
1013
|
+
flagged: args.flagged
|
|
1014
|
+
}, null, 2)
|
|
1015
|
+
}]
|
|
1016
|
+
};
|
|
1017
|
+
} finally {
|
|
1018
|
+
connection.end();
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
case 'mark_seen': {
|
|
1023
|
+
const folder = args.folder || 'INBOX';
|
|
1024
|
+
const connection = await connectIMAP();
|
|
1025
|
+
|
|
1026
|
+
try {
|
|
1027
|
+
await connection.openBox(folder);
|
|
1028
|
+
|
|
1029
|
+
if (args.seen) {
|
|
1030
|
+
await connection.addFlags(args.uid, ['\\Seen']);
|
|
1031
|
+
} else {
|
|
1032
|
+
await connection.delFlags(args.uid, ['\\Seen']);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
return {
|
|
1036
|
+
content: [{
|
|
1037
|
+
type: 'text',
|
|
1038
|
+
text: JSON.stringify({
|
|
1039
|
+
success: true,
|
|
1040
|
+
uid: args.uid,
|
|
1041
|
+
folder,
|
|
1042
|
+
seen: args.seen
|
|
1043
|
+
}, null, 2)
|
|
1044
|
+
}]
|
|
1045
|
+
};
|
|
1046
|
+
} finally {
|
|
1047
|
+
connection.end();
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
849
1051
|
default:
|
|
850
1052
|
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
|
|
851
1053
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lengelhard/imap-email-mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|