@lengelhard/imap-email-mcp 1.2.1 → 1.3.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 +142 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -461,11 +461,44 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
461
461
|
},
|
|
462
462
|
required: ['uid', 'seen']
|
|
463
463
|
}
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
name: 'move_thread',
|
|
467
|
+
description: 'Move an entire email thread (all messages matching a subject) from one folder to another in a single call. Strips Re:/Fwd: prefixes for matching. Use move_email for single-message precision moves.',
|
|
468
|
+
inputSchema: {
|
|
469
|
+
type: 'object',
|
|
470
|
+
properties: {
|
|
471
|
+
subject: {
|
|
472
|
+
type: 'string',
|
|
473
|
+
description: 'Subject line of the thread to move. Re:/Fwd: prefixes are stripped automatically for matching.'
|
|
474
|
+
},
|
|
475
|
+
from_folder: {
|
|
476
|
+
type: 'string',
|
|
477
|
+
description: 'Source folder to search for thread messages'
|
|
478
|
+
},
|
|
479
|
+
to_folder: {
|
|
480
|
+
type: 'string',
|
|
481
|
+
description: 'Destination folder to move messages into'
|
|
482
|
+
},
|
|
483
|
+
from_address: {
|
|
484
|
+
type: 'string',
|
|
485
|
+
description: 'Optional sender address to disambiguate threads with generic subjects'
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
required: ['subject', 'from_folder', 'to_folder']
|
|
489
|
+
}
|
|
464
490
|
}
|
|
465
491
|
]
|
|
466
492
|
};
|
|
467
493
|
});
|
|
468
494
|
|
|
495
|
+
// Strip Re:/Fwd:/Fw: prefixes from a subject line for thread matching.
|
|
496
|
+
// Handles nested prefixes like "Re: Re: Fwd: topic" → "topic"
|
|
497
|
+
function normalizeSubject(subject) {
|
|
498
|
+
if (!subject) return '';
|
|
499
|
+
return subject.replace(/^(\s*(Re|Fwd|Fw)\s*:\s*)+/i, '').trim();
|
|
500
|
+
}
|
|
501
|
+
|
|
469
502
|
// Helper function to connect to IMAP
|
|
470
503
|
async function connectIMAP() {
|
|
471
504
|
if (!IMAP_CONFIG.imap.password) {
|
|
@@ -1108,6 +1141,115 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1108
1141
|
}
|
|
1109
1142
|
}
|
|
1110
1143
|
|
|
1144
|
+
case 'move_thread': {
|
|
1145
|
+
const { subject, from_folder, to_folder, from_address } = args;
|
|
1146
|
+
const normalizedTarget = normalizeSubject(subject);
|
|
1147
|
+
|
|
1148
|
+
if (!normalizedTarget) {
|
|
1149
|
+
return {
|
|
1150
|
+
content: [{ type: 'text', text: 'Error: subject cannot be empty' }],
|
|
1151
|
+
isError: true
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const connection = await connectIMAP();
|
|
1156
|
+
|
|
1157
|
+
try {
|
|
1158
|
+
await connection.openBox(from_folder);
|
|
1159
|
+
|
|
1160
|
+
// Build IMAP search criteria: match subject header (and optionally sender)
|
|
1161
|
+
const searchCriteria = [['SUBJECT', normalizedTarget]];
|
|
1162
|
+
if (from_address) {
|
|
1163
|
+
searchCriteria.push(['FROM', from_address]);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const fetchOptions = {
|
|
1167
|
+
bodies: ['HEADER.FIELDS (SUBJECT FROM)'],
|
|
1168
|
+
struct: false
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
const messages = await connection.search(searchCriteria, fetchOptions);
|
|
1172
|
+
|
|
1173
|
+
if (messages.length === 0) {
|
|
1174
|
+
return {
|
|
1175
|
+
content: [{
|
|
1176
|
+
type: 'text',
|
|
1177
|
+
text: JSON.stringify({
|
|
1178
|
+
success: false,
|
|
1179
|
+
moved: 0,
|
|
1180
|
+
failed: 0,
|
|
1181
|
+
from_folder,
|
|
1182
|
+
to_folder,
|
|
1183
|
+
message: `No messages found matching subject "${normalizedTarget}" in ${from_folder}`
|
|
1184
|
+
}, null, 2)
|
|
1185
|
+
}]
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// IMAP SUBJECT search is a substring match, so post-filter to ensure
|
|
1190
|
+
// each message's normalized subject matches the target exactly.
|
|
1191
|
+
const matchingUIDs = [];
|
|
1192
|
+
for (const msg of messages) {
|
|
1193
|
+
const header = msg.parts.find(p => p.which.includes('HEADER'))?.body || {};
|
|
1194
|
+
const msgSubject = normalizeSubject((header.subject || [])[0] || '');
|
|
1195
|
+
if (msgSubject.toLowerCase() === normalizedTarget.toLowerCase()) {
|
|
1196
|
+
matchingUIDs.push(msg.attributes.uid);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
if (matchingUIDs.length === 0) {
|
|
1201
|
+
return {
|
|
1202
|
+
content: [{
|
|
1203
|
+
type: 'text',
|
|
1204
|
+
text: JSON.stringify({
|
|
1205
|
+
success: false,
|
|
1206
|
+
moved: 0,
|
|
1207
|
+
failed: 0,
|
|
1208
|
+
from_folder,
|
|
1209
|
+
to_folder,
|
|
1210
|
+
message: `IMAP returned results but none matched the exact subject "${normalizedTarget}" after normalization`
|
|
1211
|
+
}, null, 2)
|
|
1212
|
+
}]
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Move each matching UID using the same imap.move() pattern as move_email
|
|
1217
|
+
let moved = 0;
|
|
1218
|
+
const failures = [];
|
|
1219
|
+
for (const uid of matchingUIDs) {
|
|
1220
|
+
try {
|
|
1221
|
+
await new Promise((resolve, reject) => {
|
|
1222
|
+
connection.imap.move(uid, to_folder, (err) => {
|
|
1223
|
+
if (err) reject(err);
|
|
1224
|
+
else resolve();
|
|
1225
|
+
});
|
|
1226
|
+
});
|
|
1227
|
+
moved++;
|
|
1228
|
+
} catch (moveErr) {
|
|
1229
|
+
failures.push({ uid, error: moveErr.message });
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
return {
|
|
1234
|
+
content: [{
|
|
1235
|
+
type: 'text',
|
|
1236
|
+
text: JSON.stringify({
|
|
1237
|
+
success: failures.length === 0,
|
|
1238
|
+
moved,
|
|
1239
|
+
failed: failures.length,
|
|
1240
|
+
total_found: matchingUIDs.length,
|
|
1241
|
+
from_folder,
|
|
1242
|
+
to_folder,
|
|
1243
|
+
subject: normalizedTarget,
|
|
1244
|
+
...(failures.length > 0 && { failures })
|
|
1245
|
+
}, null, 2)
|
|
1246
|
+
}]
|
|
1247
|
+
};
|
|
1248
|
+
} finally {
|
|
1249
|
+
connection.end();
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1111
1253
|
default:
|
|
1112
1254
|
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
|
|
1113
1255
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lengelhard/imap-email-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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",
|