@easyasstudio/imap-email-mcp 1.1.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/.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,841 @@
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
+ import path from 'node:path';
22
+ import fs from 'node:fs';
23
+
24
+ // Configuration from environment variables
25
+ const IMAP_CONFIG = {
26
+ imap: {
27
+ user: process.env.IMAP_USER,
28
+ password: process.env.IMAP_PASSWORD,
29
+ host: process.env.IMAP_HOST,
30
+ port: parseInt(process.env.IMAP_PORT || '993'),
31
+ tls: process.env.IMAP_TLS !== 'false',
32
+ authTimeout: parseInt(process.env.IMAP_AUTH_TIMEOUT || '10000'),
33
+ tlsOptions: {
34
+ rejectUnauthorized: process.env.IMAP_TLS_REJECT_UNAUTHORIZED !== 'false'
35
+ }
36
+ }
37
+ };
38
+
39
+ const SMTP_CONFIG = {
40
+ host: process.env.SMTP_HOST,
41
+ port: parseInt(process.env.SMTP_PORT || '465'),
42
+ secure: process.env.SMTP_SECURE !== 'false',
43
+ auth: {
44
+ user: process.env.SMTP_USER || process.env.IMAP_USER,
45
+ pass: process.env.SMTP_PASSWORD || process.env.IMAP_PASSWORD
46
+ }
47
+ };
48
+
49
+ // Validate required configuration
50
+ function validateConfig() {
51
+ const required = ['IMAP_USER', 'IMAP_PASSWORD', 'IMAP_HOST'];
52
+ const missing = required.filter(key => !process.env[key]);
53
+
54
+ if (missing.length > 0) {
55
+ console.error(`Missing required environment variables: ${missing.join(', ')}`);
56
+ console.error('Please set these variables before starting the server.');
57
+ process.exit(1);
58
+ }
59
+
60
+ // SMTP host defaults to IMAP host if not set
61
+ if (!process.env.SMTP_HOST) {
62
+ SMTP_CONFIG.host = process.env.IMAP_HOST;
63
+ }
64
+ }
65
+
66
+ const server = new Server(
67
+ {
68
+ name: 'imap-email-mcp',
69
+ version: '1.0.0',
70
+ },
71
+ {
72
+ capabilities: {
73
+ tools: {},
74
+ },
75
+ }
76
+ );
77
+
78
+ // Tool definitions
79
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
80
+ return {
81
+ tools: [
82
+ {
83
+ name: 'list_folders',
84
+ description: 'List all email folders/mailboxes in the IMAP account',
85
+ inputSchema: {
86
+ type: 'object',
87
+ properties: {},
88
+ required: []
89
+ }
90
+ },
91
+ {
92
+ name: 'list_emails',
93
+ description: 'List emails from a folder with optional filtering',
94
+ inputSchema: {
95
+ type: 'object',
96
+ properties: {
97
+ folder: {
98
+ type: 'string',
99
+ description: 'Folder name (default: INBOX)',
100
+ default: 'INBOX'
101
+ },
102
+ limit: {
103
+ type: 'number',
104
+ description: 'Maximum number of emails to return (default: 20)',
105
+ default: 20
106
+ },
107
+ unseen_only: {
108
+ type: 'boolean',
109
+ description: 'Only return unread emails',
110
+ default: false
111
+ },
112
+ since_date: {
113
+ type: 'string',
114
+ description: 'Only return emails since this date (YYYY-MM-DD format)'
115
+ }
116
+ },
117
+ required: []
118
+ }
119
+ },
120
+ {
121
+ name: 'get_email',
122
+ description: 'Get full email content by UID',
123
+ inputSchema: {
124
+ type: 'object',
125
+ properties: {
126
+ uid: {
127
+ type: 'number',
128
+ description: 'Email UID'
129
+ },
130
+ folder: {
131
+ type: 'string',
132
+ description: 'Folder name (default: INBOX)',
133
+ default: 'INBOX'
134
+ }
135
+ },
136
+ required: ['uid']
137
+ }
138
+ },
139
+ {
140
+ name: 'search_emails',
141
+ description: 'Search emails by subject, from, or body text',
142
+ inputSchema: {
143
+ type: 'object',
144
+ properties: {
145
+ folder: {
146
+ type: 'string',
147
+ description: 'Folder to search (default: INBOX)',
148
+ default: 'INBOX'
149
+ },
150
+ subject: {
151
+ type: 'string',
152
+ description: 'Search in subject line'
153
+ },
154
+ from: {
155
+ type: 'string',
156
+ description: 'Search by sender'
157
+ },
158
+ body: {
159
+ type: 'string',
160
+ description: 'Search in body text'
161
+ },
162
+ limit: {
163
+ type: 'number',
164
+ description: 'Maximum results (default: 20)',
165
+ default: 20
166
+ }
167
+ },
168
+ required: []
169
+ }
170
+ },
171
+ {
172
+ name: 'list_drafts',
173
+ description: 'List all draft emails',
174
+ inputSchema: {
175
+ type: 'object',
176
+ properties: {
177
+ limit: {
178
+ type: 'number',
179
+ description: 'Maximum number of drafts to return (default: 20)',
180
+ default: 20
181
+ }
182
+ },
183
+ required: []
184
+ }
185
+ },
186
+ {
187
+ name: 'get_draft',
188
+ description: 'Get a specific draft email by UID',
189
+ inputSchema: {
190
+ type: 'object',
191
+ properties: {
192
+ uid: {
193
+ type: 'number',
194
+ description: 'Draft UID'
195
+ }
196
+ },
197
+ required: ['uid']
198
+ }
199
+ },
200
+ {
201
+ name: 'create_draft',
202
+ description: 'Create a new draft email',
203
+ inputSchema: {
204
+ type: 'object',
205
+ properties: {
206
+ to: {
207
+ type: 'string',
208
+ description: 'Recipient email address(es), comma-separated'
209
+ },
210
+ subject: {
211
+ type: 'string',
212
+ description: 'Email subject'
213
+ },
214
+ body: {
215
+ type: 'string',
216
+ description: 'Email body (plain text)'
217
+ },
218
+ html: {
219
+ type: 'string',
220
+ description: 'Email body (HTML)'
221
+ },
222
+ cc: {
223
+ type: 'string',
224
+ description: 'CC recipients, comma-separated'
225
+ },
226
+ bcc: {
227
+ type: 'string',
228
+ description: 'BCC recipients, comma-separated'
229
+ }
230
+ },
231
+ required: ['to', 'subject']
232
+ }
233
+ },
234
+ {
235
+ name: 'update_draft',
236
+ description: 'Update an existing draft by deleting old and creating new',
237
+ inputSchema: {
238
+ type: 'object',
239
+ properties: {
240
+ uid: {
241
+ type: 'number',
242
+ description: 'UID of draft to update'
243
+ },
244
+ to: {
245
+ type: 'string',
246
+ description: 'Recipient email address(es)'
247
+ },
248
+ subject: {
249
+ type: 'string',
250
+ description: 'Email subject'
251
+ },
252
+ body: {
253
+ type: 'string',
254
+ description: 'Email body (plain text)'
255
+ },
256
+ html: {
257
+ type: 'string',
258
+ description: 'Email body (HTML)'
259
+ },
260
+ cc: {
261
+ type: 'string',
262
+ description: 'CC recipients'
263
+ },
264
+ bcc: {
265
+ type: 'string',
266
+ description: 'BCC recipients'
267
+ }
268
+ },
269
+ required: ['uid', 'to', 'subject']
270
+ }
271
+ },
272
+ {
273
+ name: 'send_email',
274
+ description: 'Send an email directly',
275
+ inputSchema: {
276
+ type: 'object',
277
+ properties: {
278
+ to: {
279
+ type: 'string',
280
+ description: 'Recipient email address(es)'
281
+ },
282
+ subject: {
283
+ type: 'string',
284
+ description: 'Email subject'
285
+ },
286
+ body: {
287
+ type: 'string',
288
+ description: 'Email body (plain text)'
289
+ },
290
+ html: {
291
+ type: 'string',
292
+ description: 'Email body (HTML)'
293
+ },
294
+ cc: {
295
+ type: 'string',
296
+ description: 'CC recipients'
297
+ },
298
+ bcc: {
299
+ type: 'string',
300
+ description: 'BCC recipients'
301
+ }
302
+ },
303
+ required: ['to', 'subject']
304
+ }
305
+ },
306
+ {
307
+ name: 'download_attachment',
308
+ description: 'Download a specific attachment from an email to a local file',
309
+ inputSchema: {
310
+ type: 'object',
311
+ properties: {
312
+ uid: {
313
+ type: 'number',
314
+ description: 'Email UID'
315
+ },
316
+ attachment_index: {
317
+ type: 'number',
318
+ description: 'Attachment index (0-based, default: 0)',
319
+ default: 0
320
+ },
321
+ filename: {
322
+ type: 'string',
323
+ description: 'Custom filename for the downloaded file (optional)'
324
+ },
325
+ output_dir: {
326
+ type: 'string',
327
+ description: 'Output directory (default: working directory)'
328
+ },
329
+ folder: {
330
+ type: 'string',
331
+ description: 'Folder name (default: INBOX)',
332
+ default: 'INBOX'
333
+ }
334
+ },
335
+ required: ['uid']
336
+ }
337
+ },
338
+ {
339
+ name: 'delete_email',
340
+ description: 'Delete an email by UID',
341
+ inputSchema: {
342
+ type: 'object',
343
+ properties: {
344
+ uid: {
345
+ type: 'number',
346
+ description: 'Email UID to delete'
347
+ },
348
+ folder: {
349
+ type: 'string',
350
+ description: 'Folder name (default: INBOX)',
351
+ default: 'INBOX'
352
+ }
353
+ },
354
+ required: ['uid']
355
+ }
356
+ }
357
+ ]
358
+ };
359
+ });
360
+
361
+ // Helper function to connect to IMAP
362
+ async function connectIMAP() {
363
+ if (!IMAP_CONFIG.imap.password) {
364
+ throw new Error('IMAP_PASSWORD environment variable is not set');
365
+ }
366
+ const connection = await imaps.connect(IMAP_CONFIG);
367
+ // Send IMAP ID command (required by 163.com and other Chinese email providers)
368
+ await new Promise((resolve, reject) => {
369
+ connection.imap._enqueue('ID ("name" "imap-email-mcp" "version" "1.0" "vendor" "OpenClaw")');
370
+ connection.imap.once('close', reject);
371
+ setTimeout(resolve, 500);
372
+ });
373
+ return connection;
374
+ }
375
+
376
+ // Helper to find drafts folder (handles different provider conventions)
377
+ async function findDraftsFolder(connection) {
378
+ const boxes = await connection.getBoxes();
379
+
380
+ // Common drafts folder names across providers
381
+ const draftNames = [
382
+ 'Drafts',
383
+ 'INBOX.Drafts',
384
+ '[Gmail]/Drafts',
385
+ '[Google Mail]/Drafts',
386
+ 'Draft',
387
+ 'INBOX/Drafts'
388
+ ];
389
+
390
+ for (const name of draftNames) {
391
+ if (boxes[name] || name.split('.').reduce((acc, part) => acc?.[part], boxes)) {
392
+ return name;
393
+ }
394
+ }
395
+
396
+ // Check for nested Drafts under INBOX
397
+ if (boxes.INBOX && boxes.INBOX.children && boxes.INBOX.children.Drafts) {
398
+ return 'INBOX.Drafts';
399
+ }
400
+
401
+ return 'Drafts'; // Default fallback
402
+ }
403
+
404
+ // Tool handlers
405
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
406
+ const { name, arguments: args } = request.params;
407
+
408
+ try {
409
+ switch (name) {
410
+ case 'list_folders': {
411
+ const connection = await connectIMAP();
412
+ try {
413
+ const boxes = await connection.getBoxes();
414
+ const folders = [];
415
+
416
+ function extractFolders(obj, prefix = '') {
417
+ for (const [key, value] of Object.entries(obj)) {
418
+ const fullPath = prefix ? `${prefix}.${key}` : key;
419
+ folders.push(fullPath);
420
+ if (value.children) {
421
+ extractFolders(value.children, fullPath);
422
+ }
423
+ }
424
+ }
425
+
426
+ extractFolders(boxes);
427
+ return { content: [{ type: 'text', text: JSON.stringify(folders, null, 2) }] };
428
+ } finally {
429
+ connection.end();
430
+ }
431
+ }
432
+
433
+ case 'list_emails': {
434
+ const folder = args.folder || 'INBOX';
435
+ const limit = args.limit || 20;
436
+ const connection = await connectIMAP();
437
+
438
+ try {
439
+ await connection.openBox(folder);
440
+
441
+ let searchCriteria = ['ALL'];
442
+ if (args.unseen_only) {
443
+ searchCriteria = ['UNSEEN'];
444
+ }
445
+ if (args.since_date) {
446
+ searchCriteria = [['SINCE', args.since_date]];
447
+ }
448
+
449
+ const fetchOptions = {
450
+ bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)'],
451
+ struct: true
452
+ };
453
+
454
+ const messages = await connection.search(searchCriteria, fetchOptions);
455
+ const results = messages.slice(-limit).reverse().map(msg => {
456
+ const header = msg.parts.find(p => p.which.includes('HEADER'))?.body || {};
457
+ return {
458
+ uid: msg.attributes.uid,
459
+ date: header.date?.[0],
460
+ from: header.from?.[0],
461
+ to: header.to?.[0],
462
+ subject: header.subject?.[0],
463
+ flags: msg.attributes.flags
464
+ };
465
+ });
466
+
467
+ return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
468
+ } finally {
469
+ connection.end();
470
+ }
471
+ }
472
+
473
+ case 'get_email': {
474
+ const folder = args.folder || 'INBOX';
475
+ const connection = await connectIMAP();
476
+
477
+ try {
478
+ await connection.openBox(folder);
479
+
480
+ const fetchOptions = {
481
+ bodies: [''],
482
+ struct: true
483
+ };
484
+
485
+ const messages = await connection.search([['UID', args.uid]], fetchOptions);
486
+
487
+ if (messages.length === 0) {
488
+ return { content: [{ type: 'text', text: 'Email not found' }] };
489
+ }
490
+
491
+ const msg = messages[0];
492
+ const rawBody = msg.parts.find(p => p.which === '')?.body;
493
+ const parsed = await simpleParser(rawBody);
494
+
495
+ return {
496
+ content: [{
497
+ type: 'text',
498
+ text: JSON.stringify({
499
+ uid: msg.attributes.uid,
500
+ from: parsed.from?.text,
501
+ to: parsed.to?.text,
502
+ cc: parsed.cc?.text,
503
+ subject: parsed.subject,
504
+ date: parsed.date,
505
+ text: parsed.text,
506
+ html: parsed.html,
507
+ attachments: parsed.attachments?.map(a => ({
508
+ filename: a.filename,
509
+ contentType: a.contentType,
510
+ size: a.size
511
+ }))
512
+ }, null, 2)
513
+ }]
514
+ };
515
+ } finally {
516
+ connection.end();
517
+ }
518
+ }
519
+
520
+ case 'search_emails': {
521
+ const folder = args.folder || 'INBOX';
522
+ const limit = args.limit || 20;
523
+ const connection = await connectIMAP();
524
+
525
+ try {
526
+ await connection.openBox(folder);
527
+
528
+ let searchCriteria = [];
529
+ if (args.subject) searchCriteria.push(['SUBJECT', args.subject]);
530
+ if (args.from) searchCriteria.push(['FROM', args.from]);
531
+ if (args.body) searchCriteria.push(['BODY', args.body]);
532
+
533
+ if (searchCriteria.length === 0) {
534
+ searchCriteria = ['ALL'];
535
+ }
536
+
537
+ const fetchOptions = {
538
+ bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)'],
539
+ struct: true
540
+ };
541
+
542
+ const messages = await connection.search(searchCriteria, fetchOptions);
543
+ const results = messages.slice(-limit).reverse().map(msg => {
544
+ const header = msg.parts.find(p => p.which.includes('HEADER'))?.body || {};
545
+ return {
546
+ uid: msg.attributes.uid,
547
+ date: header.date?.[0],
548
+ from: header.from?.[0],
549
+ to: header.to?.[0],
550
+ subject: header.subject?.[0]
551
+ };
552
+ });
553
+
554
+ return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
555
+ } finally {
556
+ connection.end();
557
+ }
558
+ }
559
+
560
+ case 'list_drafts': {
561
+ const limit = args.limit || 20;
562
+ const connection = await connectIMAP();
563
+
564
+ try {
565
+ const draftsFolder = await findDraftsFolder(connection);
566
+ await connection.openBox(draftsFolder);
567
+
568
+ const fetchOptions = {
569
+ bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)'],
570
+ struct: true
571
+ };
572
+
573
+ const messages = await connection.search(['ALL'], fetchOptions);
574
+ const results = messages.slice(-limit).reverse().map(msg => {
575
+ const header = msg.parts.find(p => p.which.includes('HEADER'))?.body || {};
576
+ return {
577
+ uid: msg.attributes.uid,
578
+ date: header.date?.[0],
579
+ to: header.to?.[0],
580
+ subject: header.subject?.[0]
581
+ };
582
+ });
583
+
584
+ return { content: [{ type: 'text', text: JSON.stringify({ folder: draftsFolder, drafts: results }, null, 2) }] };
585
+ } finally {
586
+ connection.end();
587
+ }
588
+ }
589
+
590
+ case 'get_draft': {
591
+ const connection = await connectIMAP();
592
+
593
+ try {
594
+ const draftsFolder = await findDraftsFolder(connection);
595
+ await connection.openBox(draftsFolder);
596
+
597
+ const fetchOptions = {
598
+ bodies: [''],
599
+ struct: true
600
+ };
601
+
602
+ const messages = await connection.search([['UID', args.uid]], fetchOptions);
603
+
604
+ if (messages.length === 0) {
605
+ return { content: [{ type: 'text', text: 'Draft not found' }] };
606
+ }
607
+
608
+ const msg = messages[0];
609
+ const rawBody = msg.parts.find(p => p.which === '')?.body;
610
+ const parsed = await simpleParser(rawBody);
611
+
612
+ return {
613
+ content: [{
614
+ type: 'text',
615
+ text: JSON.stringify({
616
+ uid: msg.attributes.uid,
617
+ to: parsed.to?.text,
618
+ cc: parsed.cc?.text,
619
+ bcc: parsed.bcc?.text,
620
+ subject: parsed.subject,
621
+ date: parsed.date,
622
+ text: parsed.text,
623
+ html: parsed.html
624
+ }, null, 2)
625
+ }]
626
+ };
627
+ } finally {
628
+ connection.end();
629
+ }
630
+ }
631
+
632
+ case 'create_draft': {
633
+ const connection = await connectIMAP();
634
+
635
+ try {
636
+ const draftsFolder = await findDraftsFolder(connection);
637
+
638
+ // Build RFC 2822 compliant email message
639
+ const boundary = `----=_Part_${Date.now()}`;
640
+ let message = '';
641
+ message += `From: ${IMAP_CONFIG.imap.user}\r\n`;
642
+ message += `To: ${args.to}\r\n`;
643
+ if (args.cc) message += `Cc: ${args.cc}\r\n`;
644
+ if (args.bcc) message += `Bcc: ${args.bcc}\r\n`;
645
+ message += `Subject: ${args.subject}\r\n`;
646
+ message += `Date: ${new Date().toUTCString()}\r\n`;
647
+ message += `MIME-Version: 1.0\r\n`;
648
+
649
+ if (args.html) {
650
+ message += `Content-Type: multipart/alternative; boundary="${boundary}"\r\n\r\n`;
651
+ message += `--${boundary}\r\n`;
652
+ message += `Content-Type: text/plain; charset=utf-8\r\n\r\n`;
653
+ message += `${args.body || ''}\r\n`;
654
+ message += `--${boundary}\r\n`;
655
+ message += `Content-Type: text/html; charset=utf-8\r\n\r\n`;
656
+ message += `${args.html}\r\n`;
657
+ message += `--${boundary}--\r\n`;
658
+ } else {
659
+ message += `Content-Type: text/plain; charset=utf-8\r\n\r\n`;
660
+ message += `${args.body || ''}\r\n`;
661
+ }
662
+
663
+ await connection.append(message, { mailbox: draftsFolder, flags: ['\\Draft'] });
664
+
665
+ return { content: [{ type: 'text', text: `Draft created successfully in ${draftsFolder}` }] };
666
+ } finally {
667
+ connection.end();
668
+ }
669
+ }
670
+
671
+ case 'update_draft': {
672
+ const connection = await connectIMAP();
673
+
674
+ try {
675
+ const draftsFolder = await findDraftsFolder(connection);
676
+ await connection.openBox(draftsFolder);
677
+
678
+ // Delete old draft
679
+ await connection.addFlags(args.uid, ['\\Deleted']);
680
+ await connection.closeBox(true); // Expunge
681
+
682
+ // Create new draft
683
+ await connection.openBox(draftsFolder);
684
+
685
+ const boundary = `----=_Part_${Date.now()}`;
686
+ let message = '';
687
+ message += `From: ${IMAP_CONFIG.imap.user}\r\n`;
688
+ message += `To: ${args.to}\r\n`;
689
+ if (args.cc) message += `Cc: ${args.cc}\r\n`;
690
+ if (args.bcc) message += `Bcc: ${args.bcc}\r\n`;
691
+ message += `Subject: ${args.subject}\r\n`;
692
+ message += `Date: ${new Date().toUTCString()}\r\n`;
693
+ message += `MIME-Version: 1.0\r\n`;
694
+
695
+ if (args.html) {
696
+ message += `Content-Type: multipart/alternative; boundary="${boundary}"\r\n\r\n`;
697
+ message += `--${boundary}\r\n`;
698
+ message += `Content-Type: text/plain; charset=utf-8\r\n\r\n`;
699
+ message += `${args.body || ''}\r\n`;
700
+ message += `--${boundary}\r\n`;
701
+ message += `Content-Type: text/html; charset=utf-8\r\n\r\n`;
702
+ message += `${args.html}\r\n`;
703
+ message += `--${boundary}--\r\n`;
704
+ } else {
705
+ message += `Content-Type: text/plain; charset=utf-8\r\n\r\n`;
706
+ message += `${args.body || ''}\r\n`;
707
+ }
708
+
709
+ await connection.append(message, { mailbox: draftsFolder, flags: ['\\Draft'] });
710
+
711
+ return { content: [{ type: 'text', text: 'Draft updated successfully' }] };
712
+ } finally {
713
+ connection.end();
714
+ }
715
+ }
716
+
717
+ case 'send_email': {
718
+ if (!SMTP_CONFIG.host) {
719
+ return {
720
+ content: [{ type: 'text', text: 'Error: SMTP_HOST not configured. Cannot send emails.' }],
721
+ isError: true
722
+ };
723
+ }
724
+
725
+ const transporter = nodemailer.createTransport(SMTP_CONFIG);
726
+
727
+ const mailOptions = {
728
+ from: SMTP_CONFIG.auth.user,
729
+ to: args.to,
730
+ subject: args.subject,
731
+ text: args.body,
732
+ html: args.html,
733
+ cc: args.cc,
734
+ bcc: args.bcc
735
+ };
736
+
737
+ const info = await transporter.sendMail(mailOptions);
738
+
739
+ return {
740
+ content: [{
741
+ type: 'text',
742
+ text: JSON.stringify({
743
+ success: true,
744
+ messageId: info.messageId,
745
+ response: info.response
746
+ }, null, 2)
747
+ }]
748
+ };
749
+ }
750
+
751
+ case 'download_attachment': {
752
+ const folder = args.folder || 'INBOX';
753
+ const connection = await connectIMAP();
754
+
755
+ try {
756
+ await connection.openBox(folder);
757
+
758
+ const messages = await connection.search([['UID', args.uid]], {
759
+ bodies: [''],
760
+ struct: true
761
+ });
762
+
763
+ if (messages.length === 0) {
764
+ return { content: [{ type: 'text', text: 'Email not found' }] };
765
+ }
766
+
767
+ const rawBody = messages[0].parts.find(p => p.which === '')?.body;
768
+ const parsed = await simpleParser(rawBody);
769
+
770
+ if (!parsed.attachments || parsed.attachments.length === 0) {
771
+ return { content: [{ type: 'text', text: 'No attachments found in this email' }] };
772
+ }
773
+
774
+ const idx = args.attachment_index || 0;
775
+ const attachment = parsed.attachments[idx];
776
+
777
+ if (!attachment) {
778
+ return { content: [{ type: 'text', text: `Attachment index ${idx} out of range. Total: ${parsed.attachments.length}` }] };
779
+ }
780
+
781
+ const outputDir = args.output_dir || process.cwd();
782
+ const filename = args.filename || attachment.filename || `attachment_${idx}`;
783
+ const filePath = path.join(outputDir, filename);
784
+
785
+ fs.writeFileSync(filePath, attachment.content);
786
+
787
+ return {
788
+ content: [{
789
+ type: 'text',
790
+ text: JSON.stringify({
791
+ success: true,
792
+ filename: attachment.filename,
793
+ contentType: attachment.contentType,
794
+ size: attachment.size,
795
+ savedTo: filePath
796
+ }, null, 2)
797
+ }]
798
+ };
799
+ } finally {
800
+ connection.end();
801
+ }
802
+ }
803
+
804
+ case 'delete_email': {
805
+ const folder = args.folder || 'INBOX';
806
+ const connection = await connectIMAP();
807
+
808
+ try {
809
+ await connection.openBox(folder);
810
+ await connection.addFlags(args.uid, ['\\Deleted']);
811
+ await connection.closeBox(true); // Expunge
812
+
813
+ return { content: [{ type: 'text', text: 'Email deleted successfully' }] };
814
+ } finally {
815
+ connection.end();
816
+ }
817
+ }
818
+
819
+ default:
820
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
821
+ }
822
+ } catch (error) {
823
+ return {
824
+ content: [{
825
+ type: 'text',
826
+ text: `Error: ${error.message}`
827
+ }],
828
+ isError: true
829
+ };
830
+ }
831
+ });
832
+
833
+ // Start server
834
+ async function main() {
835
+ validateConfig();
836
+ const transport = new StdioServerTransport();
837
+ await server.connect(transport);
838
+ console.error('IMAP Email MCP Server running on stdio');
839
+ }
840
+
841
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@easyasstudio/imap-email-mcp",
3
+ "version": "1.1.0",
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": "easyasstudio",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/woshihoujinxin/imap-email-mcp.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/woshihoujinxin/imap-email-mcp/issues"
39
+ },
40
+ "homepage": "https://github.com/woshihoujinxin/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
+ }