@lengelhard/imap-email-mcp 1.5.0 → 1.6.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/README.md +2 -0
- package/index.js +188 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -121,6 +121,8 @@ Add to your config file:
|
|
|
121
121
|
| `update_draft` | Update an existing draft |
|
|
122
122
|
| `send_email` | Send an email directly |
|
|
123
123
|
| `delete_email` | Delete an email by UID |
|
|
124
|
+
| `download_attachment` | Download an attachment: ≤700KB returns inline base64, larger saves to disk (ATTACHMENT_DIR) |
|
|
125
|
+
| `extract_text` | Extract plain text from a PDF, xlsx/xlsm/xls, docx, or text attachment in one call, parsed server-side |
|
|
124
126
|
|
|
125
127
|
## Usage Examples
|
|
126
128
|
|
package/index.js
CHANGED
|
@@ -31,6 +31,48 @@ const ATTACHMENT_DIR =
|
|
|
31
31
|
// 700KB raw ≈ 933KB base64, leaving headroom for the JSON envelope.
|
|
32
32
|
const MAX_INLINE_BYTES = 700 * 1024;
|
|
33
33
|
|
|
34
|
+
// Cap on text returned by extract_text, to stay well under tool-result limits
|
|
35
|
+
const MAX_EXTRACT_CHARS = 500 * 1024;
|
|
36
|
+
|
|
37
|
+
// Parse a binary attachment buffer into plain text, dispatching on content
|
|
38
|
+
// type / filename extension. Parsers are imported lazily so the server starts
|
|
39
|
+
// fast and a missing optional parser only affects extract_text itself.
|
|
40
|
+
async function extractTextFromBuffer(buffer, contentType, filename) {
|
|
41
|
+
const ct = (contentType || '').toLowerCase();
|
|
42
|
+
const ext = (filename || '').toLowerCase().split('.').pop();
|
|
43
|
+
|
|
44
|
+
if (ct.includes('pdf') || ext === 'pdf') {
|
|
45
|
+
const { extractText } = await import('unpdf');
|
|
46
|
+
const { totalPages, text } = await extractText(new Uint8Array(buffer), { mergePages: true });
|
|
47
|
+
return { parser: 'unpdf', pages: totalPages, text };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
ct.includes('spreadsheetml') || ct.includes('ms-excel') ||
|
|
52
|
+
ext === 'xlsx' || ext === 'xlsm' || ext === 'xls'
|
|
53
|
+
) {
|
|
54
|
+
const XLSX = await import('xlsx');
|
|
55
|
+
const wb = XLSX.read(buffer, { type: 'buffer' });
|
|
56
|
+
const parts = wb.SheetNames.map(name => {
|
|
57
|
+
const csv = XLSX.utils.sheet_to_csv(wb.Sheets[name], { blankrows: false });
|
|
58
|
+
return `=== Sheet: ${name} ===\n${csv}`;
|
|
59
|
+
});
|
|
60
|
+
return { parser: 'xlsx', sheets: wb.SheetNames, text: parts.join('\n\n') };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (ct.includes('wordprocessingml') || ext === 'docx') {
|
|
64
|
+
const { default: mammoth } = await import('mammoth');
|
|
65
|
+
const result = await mammoth.extractRawText({ buffer });
|
|
66
|
+
return { parser: 'mammoth', text: result.value };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (ct.startsWith('text/') || ['txt', 'csv', 'md', 'json', 'xml', 'html'].includes(ext)) {
|
|
70
|
+
return { parser: 'utf8', text: buffer.toString('utf8') };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null; // unsupported type
|
|
74
|
+
}
|
|
75
|
+
|
|
34
76
|
function sanitizeFilename(name) {
|
|
35
77
|
return (name || 'attachment').replace(/[/\\?%*:|"<>\x00-\x1f]/g, '_').slice(0, 200);
|
|
36
78
|
}
|
|
@@ -426,6 +468,33 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
426
468
|
required: ['uid']
|
|
427
469
|
}
|
|
428
470
|
},
|
|
471
|
+
{
|
|
472
|
+
name: 'extract_text',
|
|
473
|
+
description: 'Extract readable plain text from an email attachment in one call, parsed server-side. Supports PDF (pdf-parse), Excel xlsx/xlsm/xls (sheet-by-sheet CSV), Word docx (mammoth), and plain-text types. Use this instead of download_attachment when the goal is to read or analyze attachment content. Returns text plus metadata (filename, contentType, size_bytes, parser). Output is capped at 500KB with a truncation note. Unsupported types return an error naming the supported set; use download_attachment for those.',
|
|
474
|
+
inputSchema: {
|
|
475
|
+
type: 'object',
|
|
476
|
+
properties: {
|
|
477
|
+
uid: {
|
|
478
|
+
type: 'number',
|
|
479
|
+
description: 'Email UID'
|
|
480
|
+
},
|
|
481
|
+
part_id: {
|
|
482
|
+
type: 'string',
|
|
483
|
+
description: 'MIME part ID of the attachment (from get_email response)'
|
|
484
|
+
},
|
|
485
|
+
filename: {
|
|
486
|
+
type: 'string',
|
|
487
|
+
description: 'Filename of the attachment (used as fallback to find part_id if not provided)'
|
|
488
|
+
},
|
|
489
|
+
folder: {
|
|
490
|
+
type: 'string',
|
|
491
|
+
description: 'Folder name the UID belongs to (default: INBOX). Always pass this explicitly for non-inbox messages.',
|
|
492
|
+
default: 'INBOX'
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
required: ['uid']
|
|
496
|
+
}
|
|
497
|
+
},
|
|
429
498
|
{
|
|
430
499
|
name: 'delete_email',
|
|
431
500
|
description: 'Delete an email by UID',
|
|
@@ -1154,6 +1223,125 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1154
1223
|
}
|
|
1155
1224
|
}
|
|
1156
1225
|
|
|
1226
|
+
case 'extract_text': {
|
|
1227
|
+
const folder = args.folder || 'INBOX';
|
|
1228
|
+
const connection = await connectIMAP();
|
|
1229
|
+
|
|
1230
|
+
try {
|
|
1231
|
+
await connection.openBox(folder);
|
|
1232
|
+
|
|
1233
|
+
const structFetch = await connection.search([['UID', args.uid]], {
|
|
1234
|
+
bodies: [],
|
|
1235
|
+
struct: true
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
if (structFetch.length === 0) {
|
|
1239
|
+
return { content: [{ type: 'text', text: 'Email not found' }] };
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const msg = structFetch[0];
|
|
1243
|
+
let partID = args.part_id;
|
|
1244
|
+
|
|
1245
|
+
if (!partID && args.filename) {
|
|
1246
|
+
const attachmentParts = findAttachmentParts(msg.attributes.struct || []);
|
|
1247
|
+
const match = attachmentParts.find(p =>
|
|
1248
|
+
(p.filename || '').toLowerCase() === args.filename.toLowerCase()
|
|
1249
|
+
);
|
|
1250
|
+
if (match) partID = match.partID;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
if (!partID) {
|
|
1254
|
+
return {
|
|
1255
|
+
content: [{ type: 'text', text: 'Could not determine part ID. Please provide part_id from get_email response.' }],
|
|
1256
|
+
isError: true
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const partFetch = await connection.search([['UID', args.uid]], {
|
|
1261
|
+
bodies: [partID],
|
|
1262
|
+
struct: true
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
if (partFetch.length === 0) {
|
|
1266
|
+
return { content: [{ type: 'text', text: 'Attachment part not found' }] };
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
const partData = partFetch[0].parts.find(p => p.which === partID);
|
|
1270
|
+
|
|
1271
|
+
if (!partData) {
|
|
1272
|
+
return { content: [{ type: 'text', text: `Part ${partID} not found in message` }] };
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
const attachmentParts = findAttachmentParts(msg.attributes.struct || []);
|
|
1276
|
+
const attachmentInfo = attachmentParts.find(p => p.partID === partID);
|
|
1277
|
+
|
|
1278
|
+
const buffer = decodePartBody(partData.body, attachmentInfo?.encoding || 'base64');
|
|
1279
|
+
const filename = sanitizeFilename(attachmentInfo?.filename || args.filename);
|
|
1280
|
+
const contentType = attachmentInfo?.contentType || 'application/octet-stream';
|
|
1281
|
+
|
|
1282
|
+
let extracted;
|
|
1283
|
+
try {
|
|
1284
|
+
extracted = await extractTextFromBuffer(buffer, contentType, filename);
|
|
1285
|
+
} catch (parseErr) {
|
|
1286
|
+
return {
|
|
1287
|
+
content: [{
|
|
1288
|
+
type: 'text',
|
|
1289
|
+
text: JSON.stringify({
|
|
1290
|
+
error: `Parsing failed: ${parseErr.message}`,
|
|
1291
|
+
uid: args.uid,
|
|
1292
|
+
partID,
|
|
1293
|
+
filename,
|
|
1294
|
+
contentType,
|
|
1295
|
+
size_bytes: buffer.length,
|
|
1296
|
+
hint: 'The file may be corrupted, password-protected, or an unusual variant. Use download_attachment to get the raw file.'
|
|
1297
|
+
}, null, 2)
|
|
1298
|
+
}],
|
|
1299
|
+
isError: true
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
if (!extracted) {
|
|
1304
|
+
return {
|
|
1305
|
+
content: [{
|
|
1306
|
+
type: 'text',
|
|
1307
|
+
text: JSON.stringify({
|
|
1308
|
+
error: `Unsupported type for text extraction: ${contentType} (${filename}). Supported: PDF, xlsx/xlsm/xls, docx, and plain-text types.`,
|
|
1309
|
+
uid: args.uid,
|
|
1310
|
+
partID,
|
|
1311
|
+
hint: 'Use download_attachment to get the raw file.'
|
|
1312
|
+
}, null, 2)
|
|
1313
|
+
}],
|
|
1314
|
+
isError: true
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
const truncated = extracted.text.length > MAX_EXTRACT_CHARS;
|
|
1319
|
+
const text = truncated
|
|
1320
|
+
? extracted.text.slice(0, MAX_EXTRACT_CHARS)
|
|
1321
|
+
: extracted.text;
|
|
1322
|
+
|
|
1323
|
+
return {
|
|
1324
|
+
content: [{
|
|
1325
|
+
type: 'text',
|
|
1326
|
+
text: JSON.stringify({
|
|
1327
|
+
uid: args.uid,
|
|
1328
|
+
partID,
|
|
1329
|
+
filename,
|
|
1330
|
+
contentType,
|
|
1331
|
+
size_bytes: buffer.length,
|
|
1332
|
+
parser: extracted.parser,
|
|
1333
|
+
...(extracted.pages !== undefined ? { pages: extracted.pages } : {}),
|
|
1334
|
+
...(extracted.sheets !== undefined ? { sheets: extracted.sheets } : {}),
|
|
1335
|
+
...(truncated ? { note: `Text truncated to ${MAX_EXTRACT_CHARS} characters (full length ${extracted.text.length}).` } : {}),
|
|
1336
|
+
text
|
|
1337
|
+
}, null, 2)
|
|
1338
|
+
}]
|
|
1339
|
+
};
|
|
1340
|
+
} finally {
|
|
1341
|
+
connection.end();
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1157
1345
|
case 'delete_email': {
|
|
1158
1346
|
const folder = args.folder || 'INBOX';
|
|
1159
1347
|
const connection = await connectIMAP();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lengelhard/imap-email-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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",
|
|
@@ -45,6 +45,9 @@
|
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
46
46
|
"imap-simple": "^5.1.0",
|
|
47
47
|
"mailparser": "^3.7.2",
|
|
48
|
-
"
|
|
48
|
+
"mammoth": "^1.8.0",
|
|
49
|
+
"nodemailer": "^6.9.16",
|
|
50
|
+
"unpdf": "^1.8.1",
|
|
51
|
+
"xlsx": "^0.18.5"
|
|
49
52
|
}
|
|
50
53
|
}
|