@lengelhard/imap-email-mcp 1.0.1

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/.env.example ADDED
@@ -0,0 +1,40 @@
1
+ # IMAP Email MCP Server Configuration
2
+ # Copy this file to .env and fill in your values
3
+
4
+ # Required - IMAP Settings
5
+ IMAP_USER=your-email@example.com
6
+ IMAP_PASSWORD=your-app-password
7
+ IMAP_HOST=imap.example.com
8
+
9
+ # Optional - IMAP Settings (defaults shown)
10
+ # IMAP_PORT=993
11
+ # IMAP_TLS=true
12
+ # IMAP_AUTH_TIMEOUT=10000
13
+ # IMAP_TLS_REJECT_UNAUTHORIZED=true
14
+
15
+ # Optional - SMTP Settings (defaults to IMAP values if not set)
16
+ # SMTP_HOST=smtp.example.com
17
+ # SMTP_PORT=465
18
+ # SMTP_SECURE=true
19
+ # SMTP_USER=your-email@example.com
20
+ # SMTP_PASSWORD=your-app-password
21
+
22
+ # Common Provider Settings:
23
+ #
24
+ # Gmail:
25
+ # IMAP_HOST=imap.gmail.com
26
+ # SMTP_HOST=smtp.gmail.com
27
+ #
28
+ # Outlook:
29
+ # IMAP_HOST=outlook.office365.com
30
+ # SMTP_HOST=smtp.office365.com
31
+ # SMTP_PORT=587
32
+ # SMTP_SECURE=false
33
+ #
34
+ # Yahoo:
35
+ # IMAP_HOST=imap.mail.yahoo.com
36
+ # SMTP_HOST=smtp.mail.yahoo.com
37
+ #
38
+ # Fastmail:
39
+ # IMAP_HOST=imap.fastmail.com
40
+ # SMTP_HOST=smtp.fastmail.com
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # IMAP Email MCP Server
2
+
3
+ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that provides email capabilities to Claude Code, Claude Desktop, Cursor, and other MCP-compatible AI tools. Connect to any IMAP/SMTP email provider to read, search, compose, and manage emails directly from your AI assistant.
4
+
5
+ ## Quick Start
6
+
7
+ ### Claude Code (CLI)
8
+
9
+ **Important:** Claude Code CLI uses `claude mcp add`, not config files.
10
+
11
+ ```bash
12
+ claude mcp add imap-email -s user \
13
+ -e IMAP_USER=you@example.com \
14
+ -e IMAP_PASSWORD='your-app-password' \
15
+ -e IMAP_HOST=imap.example.com \
16
+ -- npx -y imap-email-mcp
17
+ ```
18
+
19
+ > **Note:** If your password contains special shell characters (`%`, `^`, `*`, `$`, `!`, etc.), wrap it in single quotes as shown above.
20
+
21
+ > **Note:** Restart Claude Code after adding an MCP for the new tools to become available.
22
+
23
+ Verify with:
24
+ ```bash
25
+ claude mcp list
26
+ claude mcp get imap-email
27
+ ```
28
+
29
+ Remove with:
30
+ ```bash
31
+ claude mcp remove imap-email -s user
32
+ ```
33
+
34
+ ### Cursor
35
+
36
+ Add new MCP server:
37
+ - **Name:** `imap-email`
38
+ - **Type:** `command`
39
+ - **Command:** `npx -y imap-email-mcp`
40
+
41
+ Then set environment variables in Cursor's MCP settings:
42
+ ```
43
+ IMAP_USER=your-email@example.com
44
+ IMAP_PASSWORD=your-app-password
45
+ IMAP_HOST=imap.example.com
46
+ ```
47
+
48
+ ### Claude Desktop
49
+
50
+ Add to your config file:
51
+ - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
52
+ - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
53
+
54
+ ```json
55
+ {
56
+ "mcpServers": {
57
+ "imap-email": {
58
+ "command": "npx",
59
+ "args": ["-y", "imap-email-mcp"],
60
+ "env": {
61
+ "IMAP_USER": "your-email@example.com",
62
+ "IMAP_PASSWORD": "your-app-password",
63
+ "IMAP_HOST": "imap.example.com"
64
+ }
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ ## Features
71
+
72
+ - **Read emails** - List and read emails from any folder
73
+ - **Search** - Search by subject, sender, or body content
74
+ - **Compose** - Create and save email drafts
75
+ - **Send** - Send emails directly via SMTP
76
+ - **Manage drafts** - List, read, update, and delete drafts
77
+ - **Delete emails** - Remove unwanted messages
78
+ - **Multi-provider support** - Works with Gmail, Outlook, Yahoo, Fastmail, and any standard IMAP provider
79
+
80
+ ## Configuration
81
+
82
+ ### Required Environment Variables
83
+
84
+ | Variable | Description |
85
+ |----------|-------------|
86
+ | `IMAP_USER` | Your email address |
87
+ | `IMAP_PASSWORD` | App password (not your main password!) |
88
+ | `IMAP_HOST` | IMAP server hostname |
89
+
90
+ ### Optional Environment Variables
91
+
92
+ | Variable | Default | Description |
93
+ |----------|---------|-------------|
94
+ | `IMAP_PORT` | `993` | IMAP port |
95
+ | `IMAP_TLS` | `true` | Use TLS |
96
+ | `SMTP_HOST` | Same as IMAP_HOST | SMTP server hostname |
97
+ | `SMTP_PORT` | `465` | SMTP port |
98
+ | `SMTP_SECURE` | `true` | Use secure SMTP |
99
+
100
+ ### Provider Settings
101
+
102
+ | Provider | IMAP_HOST | SMTP_HOST | Notes |
103
+ |----------|-----------|-----------|-------|
104
+ | **Gmail** | `imap.gmail.com` | `smtp.gmail.com` | [Create App Password](https://myaccount.google.com/apppasswords) |
105
+ | **Outlook** | `outlook.office365.com` | `smtp.office365.com` | Use port 587, SMTP_SECURE=false |
106
+ | **Yahoo** | `imap.mail.yahoo.com` | `smtp.mail.yahoo.com` | Generate App Password in settings |
107
+ | **Fastmail** | `imap.fastmail.com` | `smtp.fastmail.com` | App Password from Privacy & Security |
108
+ | **iCloud** | `imap.mail.me.com` | `smtp.mail.me.com` | [Generate App Password](https://appleid.apple.com/) |
109
+
110
+ ## Available Tools
111
+
112
+ | Tool | Description |
113
+ |------|-------------|
114
+ | `list_folders` | List all email folders/mailboxes |
115
+ | `list_emails` | List emails with optional filtering |
116
+ | `get_email` | Get full email content by UID |
117
+ | `search_emails` | Search by subject, sender, or body |
118
+ | `list_drafts` | List all draft emails |
119
+ | `get_draft` | Get a specific draft by UID |
120
+ | `create_draft` | Create a new email draft |
121
+ | `update_draft` | Update an existing draft |
122
+ | `send_email` | Send an email directly |
123
+ | `delete_email` | Delete an email by UID |
124
+
125
+ ## Usage Examples
126
+
127
+ Once configured, use natural language:
128
+
129
+ - "Check my inbox for unread emails"
130
+ - "Search for emails from john@example.com"
131
+ - "Create a draft email to sarah@example.com about the meeting tomorrow"
132
+ - "Show me my drafts folder"
133
+
134
+ ## Security Best Practices
135
+
136
+ 1. **Use App Passwords** - Never use your main account password
137
+ 2. **Environment Variables** - Store credentials in env vars, not in code
138
+ 3. **Review Before Sending** - Use `create_draft` instead of `send_email` to review first
139
+
140
+ ## Troubleshooting
141
+
142
+ **Authentication failed**
143
+ - Verify your app password is correct
144
+ - Ensure IMAP access is enabled in your email provider's settings
145
+
146
+ **Drafts folder not found**
147
+ - The server tries common names (`Drafts`, `INBOX.Drafts`, `[Gmail]/Drafts`)
148
+ - Your provider may use a different folder name
149
+
150
+ **Connection timeout**
151
+ - Check your `IMAP_HOST` is correct
152
+ - Verify port 993 is not blocked by firewall
153
+
154
+ ## Alternative Installation
155
+
156
+ ### Install globally
157
+ ```bash
158
+ npm install -g imap-email-mcp
159
+ imap-email-mcp
160
+ ```
161
+
162
+ ### Clone and run
163
+ ```bash
164
+ git clone https://github.com/jdickey1/imap-email-mcp.git
165
+ cd imap-email-mcp
166
+ npm install
167
+ node index.js
168
+ ```
169
+
170
+ ## License
171
+
172
+ MIT License - see [LICENSE](LICENSE) for details.
173
+
174
+ ## Links
175
+
176
+ - [npm package](https://www.npmjs.com/package/imap-email-mcp)
177
+ - [GitHub repo](https://github.com/jdickey1/imap-email-mcp)
178
+ - [MCP Protocol](https://modelcontextprotocol.io/)
package/index.js ADDED
@@ -0,0 +1,811 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * IMAP Email MCP Server for Claude Code
5
+ *
6
+ * A Model Context Protocol (MCP) server that provides email capabilities
7
+ * through any IMAP/SMTP email provider.
8
+ *
9
+ * @license MIT
10
+ */
11
+
12
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
13
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
+ import {
15
+ CallToolRequestSchema,
16
+ ListToolsRequestSchema,
17
+ } from '@modelcontextprotocol/sdk/types.js';
18
+ import imaps from 'imap-simple';
19
+ import { simpleParser } from 'mailparser';
20
+ import nodemailer from 'nodemailer';
21
+
22
+ // Configuration from environment variables
23
+ const IMAP_CONFIG = {
24
+ imap: {
25
+ user: process.env.IMAP_USER,
26
+ password: process.env.IMAP_PASSWORD,
27
+ host: process.env.IMAP_HOST,
28
+ port: parseInt(process.env.IMAP_PORT || '993'),
29
+ tls: process.env.IMAP_TLS !== 'false',
30
+ authTimeout: parseInt(process.env.IMAP_AUTH_TIMEOUT || '10000'),
31
+ tlsOptions: {
32
+ rejectUnauthorized: process.env.IMAP_TLS_REJECT_UNAUTHORIZED !== 'false'
33
+ }
34
+ }
35
+ };
36
+
37
+ const SMTP_CONFIG = {
38
+ host: process.env.SMTP_HOST,
39
+ port: parseInt(process.env.SMTP_PORT || '465'),
40
+ secure: process.env.SMTP_SECURE !== 'false',
41
+ auth: {
42
+ user: process.env.SMTP_USER || process.env.IMAP_USER,
43
+ pass: process.env.SMTP_PASSWORD || process.env.IMAP_PASSWORD
44
+ }
45
+ };
46
+
47
+ // Traverse the raw IMAP MIME struct to find all attachment parts.
48
+ //
49
+ // Detection rules (in priority order):
50
+ // 1. Content-Disposition is "attachment" → always an attachment
51
+ // 2. Content-Disposition is "inline" or missing,
52
+ // a filename exists (from disposition params OR
53
+ // Content-Type "name" param), and the content-type
54
+ // is NOT text/plain or text/html → treat as attachment
55
+ //
56
+ // This catches real-world mailers that label attachments as
57
+ // "inline" or omit Content-Disposition entirely.
58
+ function findAttachmentParts(struct, parts = []) {
59
+ if (!Array.isArray(struct)) return parts;
60
+
61
+ for (const node of struct) {
62
+ if (Array.isArray(node)) {
63
+ // Nested multipart array — recurse
64
+ findAttachmentParts(node, parts);
65
+ continue;
66
+ }
67
+
68
+ if (typeof node !== 'object' || !node.type) continue;
69
+
70
+ const contentType = `${node.type}/${node.subtype}`.toLowerCase();
71
+ const disposition = (node.disposition?.type || '').toLowerCase();
72
+ const filename =
73
+ node.disposition?.params?.filename ||
74
+ node.disposition?.params?.name || // some clients use "name" here
75
+ node.params?.name; // fallback: Content-Type name param
76
+
77
+ const isAttachment =
78
+ disposition === 'attachment' ||
79
+ (
80
+ filename &&
81
+ contentType !== 'text/plain' &&
82
+ contentType !== 'text/html'
83
+ );
84
+
85
+ if (isAttachment) {
86
+ parts.push({ node, filename, contentType, disposition });
87
+ }
88
+ }
89
+
90
+ return parts;
91
+ }
92
+
93
+ // Validate required configuration
94
+ function validateConfig() {
95
+ const required = ['IMAP_USER', 'IMAP_PASSWORD', 'IMAP_HOST'];
96
+ const missing = required.filter(key => !process.env[key]);
97
+
98
+ if (missing.length > 0) {
99
+ console.error(`Missing required environment variables: ${missing.join(', ')}`);
100
+ console.error('Please set these variables before starting the server.');
101
+ process.exit(1);
102
+ }
103
+
104
+ // SMTP host defaults to IMAP host if not set
105
+ if (!process.env.SMTP_HOST) {
106
+ SMTP_CONFIG.host = process.env.IMAP_HOST;
107
+ }
108
+ }
109
+
110
+ const server = new Server(
111
+ {
112
+ name: 'imap-email-mcp',
113
+ version: '1.0.0',
114
+ },
115
+ {
116
+ capabilities: {
117
+ tools: {},
118
+ },
119
+ }
120
+ );
121
+
122
+ // Tool definitions
123
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
124
+ return {
125
+ tools: [
126
+ {
127
+ name: 'list_folders',
128
+ description: 'List all email folders/mailboxes in the IMAP account',
129
+ inputSchema: {
130
+ type: 'object',
131
+ properties: {},
132
+ required: []
133
+ }
134
+ },
135
+ {
136
+ name: 'list_emails',
137
+ description: 'List emails from a folder with optional filtering',
138
+ inputSchema: {
139
+ type: 'object',
140
+ properties: {
141
+ folder: {
142
+ type: 'string',
143
+ description: 'Folder name (default: INBOX)',
144
+ default: 'INBOX'
145
+ },
146
+ limit: {
147
+ type: 'number',
148
+ description: 'Maximum number of emails to return (default: 20)',
149
+ default: 20
150
+ },
151
+ unseen_only: {
152
+ type: 'boolean',
153
+ description: 'Only return unread emails',
154
+ default: false
155
+ },
156
+ since_date: {
157
+ type: 'string',
158
+ description: 'Only return emails since this date (YYYY-MM-DD format)'
159
+ }
160
+ },
161
+ required: []
162
+ }
163
+ },
164
+ {
165
+ name: 'get_email',
166
+ description: 'Get full email content by UID',
167
+ inputSchema: {
168
+ type: 'object',
169
+ properties: {
170
+ uid: {
171
+ type: 'number',
172
+ description: 'Email UID'
173
+ },
174
+ folder: {
175
+ type: 'string',
176
+ description: 'Folder name (default: INBOX)',
177
+ default: 'INBOX'
178
+ }
179
+ },
180
+ required: ['uid']
181
+ }
182
+ },
183
+ {
184
+ name: 'search_emails',
185
+ description: 'Search emails by subject, from, or body text',
186
+ inputSchema: {
187
+ type: 'object',
188
+ properties: {
189
+ folder: {
190
+ type: 'string',
191
+ description: 'Folder to search (default: INBOX)',
192
+ default: 'INBOX'
193
+ },
194
+ subject: {
195
+ type: 'string',
196
+ description: 'Search in subject line'
197
+ },
198
+ from: {
199
+ type: 'string',
200
+ description: 'Search by sender'
201
+ },
202
+ body: {
203
+ type: 'string',
204
+ description: 'Search in body text'
205
+ },
206
+ limit: {
207
+ type: 'number',
208
+ description: 'Maximum results (default: 20)',
209
+ default: 20
210
+ }
211
+ },
212
+ required: []
213
+ }
214
+ },
215
+ {
216
+ name: 'list_drafts',
217
+ description: 'List all draft emails',
218
+ inputSchema: {
219
+ type: 'object',
220
+ properties: {
221
+ limit: {
222
+ type: 'number',
223
+ description: 'Maximum number of drafts to return (default: 20)',
224
+ default: 20
225
+ }
226
+ },
227
+ required: []
228
+ }
229
+ },
230
+ {
231
+ name: 'get_draft',
232
+ description: 'Get a specific draft email by UID',
233
+ inputSchema: {
234
+ type: 'object',
235
+ properties: {
236
+ uid: {
237
+ type: 'number',
238
+ description: 'Draft UID'
239
+ }
240
+ },
241
+ required: ['uid']
242
+ }
243
+ },
244
+ {
245
+ name: 'create_draft',
246
+ description: 'Create a new draft email',
247
+ inputSchema: {
248
+ type: 'object',
249
+ properties: {
250
+ to: {
251
+ type: 'string',
252
+ description: 'Recipient email address(es), comma-separated'
253
+ },
254
+ subject: {
255
+ type: 'string',
256
+ description: 'Email subject'
257
+ },
258
+ body: {
259
+ type: 'string',
260
+ description: 'Email body (plain text)'
261
+ },
262
+ html: {
263
+ type: 'string',
264
+ description: 'Email body (HTML)'
265
+ },
266
+ cc: {
267
+ type: 'string',
268
+ description: 'CC recipients, comma-separated'
269
+ },
270
+ bcc: {
271
+ type: 'string',
272
+ description: 'BCC recipients, comma-separated'
273
+ }
274
+ },
275
+ required: ['to', 'subject']
276
+ }
277
+ },
278
+ {
279
+ name: 'update_draft',
280
+ description: 'Update an existing draft by deleting old and creating new',
281
+ inputSchema: {
282
+ type: 'object',
283
+ properties: {
284
+ uid: {
285
+ type: 'number',
286
+ description: 'UID of draft to update'
287
+ },
288
+ to: {
289
+ type: 'string',
290
+ description: 'Recipient email address(es)'
291
+ },
292
+ subject: {
293
+ type: 'string',
294
+ description: 'Email subject'
295
+ },
296
+ body: {
297
+ type: 'string',
298
+ description: 'Email body (plain text)'
299
+ },
300
+ html: {
301
+ type: 'string',
302
+ description: 'Email body (HTML)'
303
+ },
304
+ cc: {
305
+ type: 'string',
306
+ description: 'CC recipients'
307
+ },
308
+ bcc: {
309
+ type: 'string',
310
+ description: 'BCC recipients'
311
+ }
312
+ },
313
+ required: ['uid', 'to', 'subject']
314
+ }
315
+ },
316
+ {
317
+ name: 'send_email',
318
+ description: 'Send an email directly',
319
+ inputSchema: {
320
+ type: 'object',
321
+ properties: {
322
+ to: {
323
+ type: 'string',
324
+ description: 'Recipient email address(es)'
325
+ },
326
+ subject: {
327
+ type: 'string',
328
+ description: 'Email subject'
329
+ },
330
+ body: {
331
+ type: 'string',
332
+ description: 'Email body (plain text)'
333
+ },
334
+ html: {
335
+ type: 'string',
336
+ description: 'Email body (HTML)'
337
+ },
338
+ cc: {
339
+ type: 'string',
340
+ description: 'CC recipients'
341
+ },
342
+ bcc: {
343
+ type: 'string',
344
+ description: 'BCC recipients'
345
+ }
346
+ },
347
+ required: ['to', 'subject']
348
+ }
349
+ },
350
+ {
351
+ name: 'delete_email',
352
+ description: 'Delete an email by UID',
353
+ inputSchema: {
354
+ type: 'object',
355
+ properties: {
356
+ uid: {
357
+ type: 'number',
358
+ description: 'Email UID to delete'
359
+ },
360
+ folder: {
361
+ type: 'string',
362
+ description: 'Folder name (default: INBOX)',
363
+ default: 'INBOX'
364
+ }
365
+ },
366
+ required: ['uid']
367
+ }
368
+ }
369
+ ]
370
+ };
371
+ });
372
+
373
+ // Helper function to connect to IMAP
374
+ async function connectIMAP() {
375
+ if (!IMAP_CONFIG.imap.password) {
376
+ throw new Error('IMAP_PASSWORD environment variable is not set');
377
+ }
378
+ return await imaps.connect(IMAP_CONFIG);
379
+ }
380
+
381
+ // Helper to find drafts folder (handles different provider conventions)
382
+ async function findDraftsFolder(connection) {
383
+ const boxes = await connection.getBoxes();
384
+
385
+ // Common drafts folder names across providers
386
+ const draftNames = [
387
+ 'Drafts',
388
+ 'INBOX.Drafts',
389
+ '[Gmail]/Drafts',
390
+ '[Google Mail]/Drafts',
391
+ 'Draft',
392
+ 'INBOX/Drafts'
393
+ ];
394
+
395
+ for (const name of draftNames) {
396
+ if (boxes[name] || name.split('.').reduce((acc, part) => acc?.[part], boxes)) {
397
+ return name;
398
+ }
399
+ }
400
+
401
+ // Check for nested Drafts under INBOX
402
+ if (boxes.INBOX && boxes.INBOX.children && boxes.INBOX.children.Drafts) {
403
+ return 'INBOX.Drafts';
404
+ }
405
+
406
+ return 'Drafts'; // Default fallback
407
+ }
408
+
409
+ // Tool handlers
410
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
411
+ const { name, arguments: args } = request.params;
412
+
413
+ try {
414
+ switch (name) {
415
+ case 'list_folders': {
416
+ const connection = await connectIMAP();
417
+ try {
418
+ const boxes = await connection.getBoxes();
419
+ const folders = [];
420
+
421
+ function extractFolders(obj, prefix = '') {
422
+ for (const [key, value] of Object.entries(obj)) {
423
+ const fullPath = prefix ? `${prefix}.${key}` : key;
424
+ folders.push(fullPath);
425
+ if (value.children) {
426
+ extractFolders(value.children, fullPath);
427
+ }
428
+ }
429
+ }
430
+
431
+ extractFolders(boxes);
432
+ return { content: [{ type: 'text', text: JSON.stringify(folders, null, 2) }] };
433
+ } finally {
434
+ connection.end();
435
+ }
436
+ }
437
+
438
+ case 'list_emails': {
439
+ const folder = args.folder || 'INBOX';
440
+ const limit = args.limit || 20;
441
+ const connection = await connectIMAP();
442
+
443
+ try {
444
+ await connection.openBox(folder);
445
+
446
+ let searchCriteria = ['ALL'];
447
+ if (args.unseen_only) {
448
+ searchCriteria = ['UNSEEN'];
449
+ }
450
+ if (args.since_date) {
451
+ searchCriteria = [['SINCE', args.since_date]];
452
+ }
453
+
454
+ const fetchOptions = {
455
+ bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)'],
456
+ struct: true
457
+ };
458
+
459
+ const messages = await connection.search(searchCriteria, fetchOptions);
460
+ const results = messages.slice(-limit).reverse().map(msg => {
461
+ const header = msg.parts.find(p => p.which.includes('HEADER'))?.body || {};
462
+ return {
463
+ uid: msg.attributes.uid,
464
+ date: header.date?.[0],
465
+ from: header.from?.[0],
466
+ to: header.to?.[0],
467
+ subject: header.subject?.[0],
468
+ flags: msg.attributes.flags
469
+ };
470
+ });
471
+
472
+ return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
473
+ } finally {
474
+ connection.end();
475
+ }
476
+ }
477
+
478
+ case 'get_email': {
479
+ const folder = args.folder || 'INBOX';
480
+ const connection = await connectIMAP();
481
+
482
+ try {
483
+ await connection.openBox(folder);
484
+
485
+ const fetchOptions = {
486
+ bodies: [''],
487
+ struct: true
488
+ };
489
+
490
+ const messages = await connection.search([['UID', args.uid]], fetchOptions);
491
+
492
+ if (messages.length === 0) {
493
+ return { content: [{ type: 'text', text: 'Email not found' }] };
494
+ }
495
+
496
+ const msg = messages[0];
497
+ const rawBody = msg.parts.find(p => p.which === '')?.body;
498
+ const parsed = await simpleParser(rawBody);
499
+
500
+ return {
501
+ content: [{
502
+ type: 'text',
503
+ text: JSON.stringify({
504
+ uid: msg.attributes.uid,
505
+ from: parsed.from?.text,
506
+ to: parsed.to?.text,
507
+ cc: parsed.cc?.text,
508
+ subject: parsed.subject,
509
+ date: parsed.date,
510
+ text: parsed.text,
511
+ html: parsed.html,
512
+ attachments: (() => {
513
+ // Start with what mailparser found (covers Content-Disposition: attachment
514
+ // and inline+filename cases for content it fully parsed).
515
+ const fromParser = new Map(
516
+ (parsed.attachments || []).map(a => [
517
+ (a.filename || '').toLowerCase(),
518
+ { filename: a.filename, contentType: a.contentType, size: a.size }
519
+ ])
520
+ );
521
+
522
+ // Also walk the raw MIME struct so we catch parts that mailparser
523
+ // may have skipped: inline parts with filenames and parts that have
524
+ // no Content-Disposition at all but carry a Content-Type "name" param.
525
+ const fromStruct = findAttachmentParts(msg.attributes.struct || []);
526
+ for (const { filename, contentType } of fromStruct) {
527
+ const key = (filename || '').toLowerCase();
528
+ if (filename && !fromParser.has(key)) {
529
+ fromParser.set(key, { filename, contentType, size: null });
530
+ }
531
+ }
532
+
533
+ return [...fromParser.values()];
534
+ })()
535
+ }, null, 2)
536
+ }]
537
+ };
538
+ } finally {
539
+ connection.end();
540
+ }
541
+ }
542
+
543
+ case 'search_emails': {
544
+ const folder = args.folder || 'INBOX';
545
+ const limit = args.limit || 20;
546
+ const connection = await connectIMAP();
547
+
548
+ try {
549
+ await connection.openBox(folder);
550
+
551
+ let searchCriteria = [];
552
+ if (args.subject) searchCriteria.push(['SUBJECT', args.subject]);
553
+ if (args.from) searchCriteria.push(['FROM', args.from]);
554
+ if (args.body) searchCriteria.push(['BODY', args.body]);
555
+
556
+ if (searchCriteria.length === 0) {
557
+ searchCriteria = ['ALL'];
558
+ }
559
+
560
+ const fetchOptions = {
561
+ bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)'],
562
+ struct: true
563
+ };
564
+
565
+ const messages = await connection.search(searchCriteria, fetchOptions);
566
+ const results = messages.slice(-limit).reverse().map(msg => {
567
+ const header = msg.parts.find(p => p.which.includes('HEADER'))?.body || {};
568
+ return {
569
+ uid: msg.attributes.uid,
570
+ date: header.date?.[0],
571
+ from: header.from?.[0],
572
+ to: header.to?.[0],
573
+ subject: header.subject?.[0]
574
+ };
575
+ });
576
+
577
+ return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
578
+ } finally {
579
+ connection.end();
580
+ }
581
+ }
582
+
583
+ case 'list_drafts': {
584
+ const limit = args.limit || 20;
585
+ const connection = await connectIMAP();
586
+
587
+ try {
588
+ const draftsFolder = await findDraftsFolder(connection);
589
+ await connection.openBox(draftsFolder);
590
+
591
+ const fetchOptions = {
592
+ bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)'],
593
+ struct: true
594
+ };
595
+
596
+ const messages = await connection.search(['ALL'], fetchOptions);
597
+ const results = messages.slice(-limit).reverse().map(msg => {
598
+ const header = msg.parts.find(p => p.which.includes('HEADER'))?.body || {};
599
+ return {
600
+ uid: msg.attributes.uid,
601
+ date: header.date?.[0],
602
+ to: header.to?.[0],
603
+ subject: header.subject?.[0]
604
+ };
605
+ });
606
+
607
+ return { content: [{ type: 'text', text: JSON.stringify({ folder: draftsFolder, drafts: results }, null, 2) }] };
608
+ } finally {
609
+ connection.end();
610
+ }
611
+ }
612
+
613
+ case 'get_draft': {
614
+ const connection = await connectIMAP();
615
+
616
+ try {
617
+ const draftsFolder = await findDraftsFolder(connection);
618
+ await connection.openBox(draftsFolder);
619
+
620
+ const fetchOptions = {
621
+ bodies: [''],
622
+ struct: true
623
+ };
624
+
625
+ const messages = await connection.search([['UID', args.uid]], fetchOptions);
626
+
627
+ if (messages.length === 0) {
628
+ return { content: [{ type: 'text', text: 'Draft not found' }] };
629
+ }
630
+
631
+ const msg = messages[0];
632
+ const rawBody = msg.parts.find(p => p.which === '')?.body;
633
+ const parsed = await simpleParser(rawBody);
634
+
635
+ return {
636
+ content: [{
637
+ type: 'text',
638
+ text: JSON.stringify({
639
+ uid: msg.attributes.uid,
640
+ to: parsed.to?.text,
641
+ cc: parsed.cc?.text,
642
+ bcc: parsed.bcc?.text,
643
+ subject: parsed.subject,
644
+ date: parsed.date,
645
+ text: parsed.text,
646
+ html: parsed.html
647
+ }, null, 2)
648
+ }]
649
+ };
650
+ } finally {
651
+ connection.end();
652
+ }
653
+ }
654
+
655
+ case 'create_draft': {
656
+ const connection = await connectIMAP();
657
+
658
+ try {
659
+ const draftsFolder = await findDraftsFolder(connection);
660
+
661
+ // Build RFC 2822 compliant email message
662
+ const boundary = `----=_Part_${Date.now()}`;
663
+ let message = '';
664
+ message += `From: ${IMAP_CONFIG.imap.user}\r\n`;
665
+ message += `To: ${args.to}\r\n`;
666
+ if (args.cc) message += `Cc: ${args.cc}\r\n`;
667
+ if (args.bcc) message += `Bcc: ${args.bcc}\r\n`;
668
+ message += `Subject: ${args.subject}\r\n`;
669
+ message += `Date: ${new Date().toUTCString()}\r\n`;
670
+ message += `MIME-Version: 1.0\r\n`;
671
+
672
+ if (args.html) {
673
+ message += `Content-Type: multipart/alternative; boundary="${boundary}"\r\n\r\n`;
674
+ message += `--${boundary}\r\n`;
675
+ message += `Content-Type: text/plain; charset=utf-8\r\n\r\n`;
676
+ message += `${args.body || ''}\r\n`;
677
+ message += `--${boundary}\r\n`;
678
+ message += `Content-Type: text/html; charset=utf-8\r\n\r\n`;
679
+ message += `${args.html}\r\n`;
680
+ message += `--${boundary}--\r\n`;
681
+ } else {
682
+ message += `Content-Type: text/plain; charset=utf-8\r\n\r\n`;
683
+ message += `${args.body || ''}\r\n`;
684
+ }
685
+
686
+ await connection.append(message, { mailbox: draftsFolder, flags: ['\\Draft'] });
687
+
688
+ return { content: [{ type: 'text', text: `Draft created successfully in ${draftsFolder}` }] };
689
+ } finally {
690
+ connection.end();
691
+ }
692
+ }
693
+
694
+ case 'update_draft': {
695
+ const connection = await connectIMAP();
696
+
697
+ try {
698
+ const draftsFolder = await findDraftsFolder(connection);
699
+ await connection.openBox(draftsFolder);
700
+
701
+ // Delete old draft
702
+ await connection.addFlags(args.uid, ['\\Deleted']);
703
+ await connection.closeBox(true); // Expunge
704
+
705
+ // Create new draft
706
+ await connection.openBox(draftsFolder);
707
+
708
+ const boundary = `----=_Part_${Date.now()}`;
709
+ let message = '';
710
+ message += `From: ${IMAP_CONFIG.imap.user}\r\n`;
711
+ message += `To: ${args.to}\r\n`;
712
+ if (args.cc) message += `Cc: ${args.cc}\r\n`;
713
+ if (args.bcc) message += `Bcc: ${args.bcc}\r\n`;
714
+ message += `Subject: ${args.subject}\r\n`;
715
+ message += `Date: ${new Date().toUTCString()}\r\n`;
716
+ message += `MIME-Version: 1.0\r\n`;
717
+
718
+ if (args.html) {
719
+ message += `Content-Type: multipart/alternative; boundary="${boundary}"\r\n\r\n`;
720
+ message += `--${boundary}\r\n`;
721
+ message += `Content-Type: text/plain; charset=utf-8\r\n\r\n`;
722
+ message += `${args.body || ''}\r\n`;
723
+ message += `--${boundary}\r\n`;
724
+ message += `Content-Type: text/html; charset=utf-8\r\n\r\n`;
725
+ message += `${args.html}\r\n`;
726
+ message += `--${boundary}--\r\n`;
727
+ } else {
728
+ message += `Content-Type: text/plain; charset=utf-8\r\n\r\n`;
729
+ message += `${args.body || ''}\r\n`;
730
+ }
731
+
732
+ await connection.append(message, { mailbox: draftsFolder, flags: ['\\Draft'] });
733
+
734
+ return { content: [{ type: 'text', text: 'Draft updated successfully' }] };
735
+ } finally {
736
+ connection.end();
737
+ }
738
+ }
739
+
740
+ case 'send_email': {
741
+ if (!SMTP_CONFIG.host) {
742
+ return {
743
+ content: [{ type: 'text', text: 'Error: SMTP_HOST not configured. Cannot send emails.' }],
744
+ isError: true
745
+ };
746
+ }
747
+
748
+ const transporter = nodemailer.createTransport(SMTP_CONFIG);
749
+
750
+ const mailOptions = {
751
+ from: SMTP_CONFIG.auth.user,
752
+ to: args.to,
753
+ subject: args.subject,
754
+ text: args.body,
755
+ html: args.html,
756
+ cc: args.cc,
757
+ bcc: args.bcc
758
+ };
759
+
760
+ const info = await transporter.sendMail(mailOptions);
761
+
762
+ return {
763
+ content: [{
764
+ type: 'text',
765
+ text: JSON.stringify({
766
+ success: true,
767
+ messageId: info.messageId,
768
+ response: info.response
769
+ }, null, 2)
770
+ }]
771
+ };
772
+ }
773
+
774
+ case 'delete_email': {
775
+ const folder = args.folder || 'INBOX';
776
+ const connection = await connectIMAP();
777
+
778
+ try {
779
+ await connection.openBox(folder);
780
+ await connection.addFlags(args.uid, ['\\Deleted']);
781
+ await connection.closeBox(true); // Expunge
782
+
783
+ return { content: [{ type: 'text', text: 'Email deleted successfully' }] };
784
+ } finally {
785
+ connection.end();
786
+ }
787
+ }
788
+
789
+ default:
790
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
791
+ }
792
+ } catch (error) {
793
+ return {
794
+ content: [{
795
+ type: 'text',
796
+ text: `Error: ${error.message}`
797
+ }],
798
+ isError: true
799
+ };
800
+ }
801
+ });
802
+
803
+ // Start server
804
+ async function main() {
805
+ validateConfig();
806
+ const transport = new StdioServerTransport();
807
+ await server.connect(transport);
808
+ console.error('IMAP Email MCP Server running on stdio');
809
+ }
810
+
811
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@lengelhard/imap-email-mcp",
3
+ "version": "1.0.1",
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
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "imap-email-mcp": "index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js"
12
+ },
13
+ "keywords": [
14
+ "mcp",
15
+ "mcp-server",
16
+ "model-context-protocol",
17
+ "claude",
18
+ "claude-code",
19
+ "anthropic",
20
+ "email",
21
+ "imap",
22
+ "smtp",
23
+ "mail",
24
+ "inbox",
25
+ "gmail",
26
+ "outlook",
27
+ "email-client",
28
+ "ai-tools",
29
+ "llm-tools"
30
+ ],
31
+ "author": "",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/lengelhard/imap-email-mcp.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/lengelhard/imap-email-mcp/issues"
39
+ },
40
+ "homepage": "https://github.com/lengelhard/imap-email-mcp#readme",
41
+ "engines": {
42
+ "node": ">=18.0.0"
43
+ },
44
+ "dependencies": {
45
+ "@modelcontextprotocol/sdk": "^1.0.0",
46
+ "imap-simple": "^5.1.0",
47
+ "mailparser": "^3.7.2",
48
+ "nodemailer": "^6.9.16"
49
+ }
50
+ }