mcp-mail-server 1.1.12 → 1.1.14

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.
Files changed (3) hide show
  1. package/README.md +387 -159
  2. package/dist/index.js +1 -1
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -1,78 +1,268 @@
1
- # MCP Mail Server
1
+ # Mail MCP Server
2
2
 
3
- ![NPM Version](https://img.shields.io/npm/v/mcp-mail-server)
3
+ <!-- mcp-name: mcp-mail-server -->
4
+
5
+ [![NPM Version](https://img.shields.io/npm/v/mcp-mail-server)](https://www.npmjs.com/package/mcp-mail-server)
4
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
7
 
6
8
  **Language:** English | [中文](README-zh.md)
7
9
 
8
- A Model Context Protocol server for IMAP/SMTP email operations with Claude, Cursor, and other AI assistants.
9
-
10
- ## Features
11
-
12
- - **IMAP Operations**: Search, read, and manage emails across mailboxes
13
- - **SMTP Support**: Send emails with HTML/text content and attachments
14
- - **Secure Configuration**: Environment-based setup with TLS/SSL support
15
- - **AI-Friendly**: Natural language commands for email operations
16
- - **Auto Connection Management**: Automatic IMAP/SMTP connection handling
17
- - **Multi-Mailbox Support**: Access INBOX, Sent, and custom folders
18
-
19
- ## Quick Start
20
-
21
- 1. **Install**: `npm install -g mcp-mail-server`
22
- 2. **Configure** environment variables (see [Configuration](#configuration))
23
- 3. **Add** to your MCP client configuration
24
- 4. **Use** natural language: *"Show me unread emails from today"*
10
+ A Model Context Protocol (MCP) server that provides email capabilities through IMAP and SMTP protocols. This server enables LLMs to search, read, manage, and send emails on behalf of users.
11
+
12
+ > [!NOTE]
13
+ > This server requires access to your email account credentials. Use **app-specific passwords** whenever possible, and never hardcode credentials. Exercise caution to ensure your email data is handled securely.
14
+
15
+ ### Features
16
+
17
+ - Read/search/delete emails via IMAP
18
+ - Send and reply to emails via SMTP
19
+ - Read and export email attachments to local files
20
+ - Multi-mailbox support (INBOX, Sent, custom folders)
21
+ - Automatic IMAP/SMTP connection management
22
+ - Secure TLS/SSL connections
23
+
24
+ ### Tools
25
+
26
+ - **connect_all**
27
+ - Connect to both IMAP and SMTP servers
28
+ - No parameters required
29
+
30
+ - **get_connection_status**
31
+ - Check current connection status and server info
32
+ - No parameters required
33
+
34
+ - **disconnect_all**
35
+ - Disconnect from all email servers
36
+ - No parameters required
37
+
38
+ - **open_mailbox**
39
+ - Open a specific mailbox/folder
40
+ - Inputs:
41
+ - `mailboxName` (string, optional): Folder name (default: "INBOX")
42
+ - `readOnly` (boolean, optional): Open in read-only mode
43
+
44
+ - **list_mailboxes**
45
+ - List all available mail folders
46
+ - No parameters required
47
+
48
+ - **search_messages**
49
+ - Search emails using IMAP search criteria
50
+ - Inputs:
51
+ - `criteria` (array): IMAP search criteria
52
+
53
+ - **search_by_sender**
54
+ - Find emails from a specific sender
55
+ - Inputs:
56
+ - `sender` (string): Email address of sender
57
+
58
+ - **search_by_subject**
59
+ - Search emails by subject keywords
60
+ - Inputs:
61
+ - `subject` (string): Subject keywords
62
+
63
+ - **search_by_body**
64
+ - Search within email message content
65
+ - Inputs:
66
+ - `text` (string): Search text
67
+
68
+ - **search_since_date**
69
+ - Find emails since a specific date
70
+ - Inputs:
71
+ - `date` (string): Date string
72
+
73
+ - **search_unreplied_from_sender**
74
+ - Find unreplied emails from a specific sender
75
+ - Inputs:
76
+ - `sender` (string): Email address
77
+ - `startDate` (string, optional): Start date filter
78
+ - `endDate` (string, optional): End date filter
79
+
80
+ - **search_larger_than**
81
+ - Find emails larger than a specific size
82
+ - Inputs:
83
+ - `size` (number): Size threshold in bytes
84
+
85
+ - **get_message**
86
+ - Retrieve a single email by UID
87
+ - Inputs:
88
+ - `uid` (number): Email UID
89
+ - `markSeen` (boolean, optional): Mark as read
90
+ - `includeAttachmentContent` (boolean, optional): Include attachment data (default: true)
91
+ - `attachmentMaxBytes` (number, optional): Max attachment size to include
92
+
93
+ - **get_messages**
94
+ - Retrieve multiple emails by UIDs
95
+ - Inputs:
96
+ - `uids` (number[]): Array of email UIDs
97
+ - `markSeen` (boolean, optional): Mark as read
98
+ - `includeAttachmentContent` (boolean, optional): Include attachment data (default: false)
99
+ - `attachmentMaxBytes` (number, optional): Max attachment size to include
100
+
101
+ Attachment fields returned in message objects:
102
+ - `attachments[].filename`
103
+ - `attachments[].contentType`
104
+ - `attachments[].size`
105
+ - `attachments[].contentBase64` (only when `includeAttachmentContent=true` and within size limit)
106
+ - `attachments[].contentTruncated` (true when attachment exceeds `attachmentMaxBytes`)
107
+
108
+ - **export_attachment**
109
+ - Export an email attachment to a local file
110
+ - Inputs:
111
+ - `uid` (number): Email UID
112
+ - `filePath` (string): Destination file path
113
+ - `attachmentIndex` (number, optional): Attachment index (default: 0)
114
+ - `filename` (string, optional): Match attachment by filename
115
+
116
+ - **delete_message**
117
+ - Delete an email by UID
118
+ - Inputs:
119
+ - `uid` (number): Email UID
120
+
121
+ - **get_unseen_messages**
122
+ - Get all unread emails
123
+ - No parameters required
124
+
125
+ - **get_recent_messages**
126
+ - Get recently received emails
127
+ - No parameters required
128
+
129
+ - **send_email**
130
+ - Send an email via SMTP
131
+ - Inputs:
132
+ - `to` (string, required): Recipient email address
133
+ - `subject` (string, required): Email subject
134
+ - `text` (string, optional): Plain text body
135
+ - `html` (string, optional): HTML body
136
+ - `cc` (string, optional): CC recipients
137
+ - `bcc` (string, optional): BCC recipients
138
+ - `attachments` (array, optional): File attachments
139
+ - `filename` (string, required): Attachment filename
140
+ - `content` (string, required): File content
141
+ - `contentType` (string, optional): MIME type
142
+ - `encoding` ("utf8" | "base64", optional): Content encoding (default: "utf8")
143
+
144
+ - **reply_to_email**
145
+ - Reply to a specific email
146
+ - Inputs:
147
+ - `originalUid` (number, required): UID of the email to reply to
148
+ - `text` (string, required): Reply text body
149
+ - `html` (string, optional): Reply HTML body
150
+ - `replyToAll` (boolean, optional): Reply to all recipients
151
+ - `includeOriginal` (boolean, optional): Include original message
25
152
 
26
153
  ## Installation
27
154
 
28
155
  <details>
29
156
  <summary>Claude Desktop</summary>
30
157
 
31
- Add to your `claude_desktop_config.json`:
158
+ Add this to your `claude_desktop_config.json`:
32
159
 
33
160
  ```json
34
161
  {
35
162
  "mcpServers": {
36
163
  "mcp-mail-server": {
37
164
  "command": "npx",
38
- "args": ["mcp-mail-server"],
165
+ "args": ["-y", "mcp-mail-server"],
39
166
  "env": {
40
- "IMAP_HOST": "your-imap-server.com",
167
+ "IMAP_HOST": "imap.gmail.com",
41
168
  "IMAP_PORT": "993",
42
169
  "IMAP_SECURE": "true",
43
- "SMTP_HOST": "your-smtp-server.com",
170
+ "SMTP_HOST": "smtp.gmail.com",
44
171
  "SMTP_PORT": "465",
45
172
  "SMTP_SECURE": "true",
46
- "EMAIL_USER": "your-email@domain.com",
47
- "EMAIL_PASS": "your-password"
173
+ "EMAIL_USER": "your-email@gmail.com",
174
+ "EMAIL_PASS": "your-app-password"
175
+ }
176
+ }
177
+ }
178
+ }
179
+ ```
180
+
181
+ </details>
182
+
183
+ <details>
184
+ <summary>Claude Code</summary>
185
+
186
+ Run the following command in your terminal:
187
+
188
+ ```bash
189
+ claude mcp add mail \
190
+ -e IMAP_HOST=imap.gmail.com \
191
+ -e IMAP_PORT=993 \
192
+ -e IMAP_SECURE=true \
193
+ -e SMTP_HOST=smtp.gmail.com \
194
+ -e SMTP_PORT=465 \
195
+ -e SMTP_SECURE=true \
196
+ -e EMAIL_USER=your-email@gmail.com \
197
+ -e EMAIL_PASS=your-app-password \
198
+ -- npx -y mcp-mail-server
199
+ ```
200
+
201
+ For more details, see the [Claude Code MCP documentation](https://docs.anthropic.com/en/docs/claude-code/mcp).
202
+
203
+ </details>
204
+
205
+ <details>
206
+ <summary>VS Code</summary>
207
+
208
+ For quick installation, click the install button below:
209
+
210
+ [![Install with NPX in VS Code](https://img.shields.io/badge/VS_Code-NPM-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=mail&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22mcp-mail-server%22%5D%7D)
211
+ [![Install with NPX in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-NPM-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=mail&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22mcp-mail-server%22%5D%7D&quality=insiders)
212
+
213
+ For manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`.
214
+
215
+ Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
216
+
217
+ > Note that the `mcp` key is needed when using the `mcp.json` file.
218
+
219
+ ```json
220
+ {
221
+ "mcp": {
222
+ "servers": {
223
+ "mcp-mail-server": {
224
+ "command": "npx",
225
+ "args": ["-y", "mcp-mail-server"],
226
+ "env": {
227
+ "IMAP_HOST": "imap.gmail.com",
228
+ "IMAP_PORT": "993",
229
+ "IMAP_SECURE": "true",
230
+ "SMTP_HOST": "smtp.gmail.com",
231
+ "SMTP_PORT": "465",
232
+ "SMTP_SECURE": "true",
233
+ "EMAIL_USER": "your-email@gmail.com",
234
+ "EMAIL_PASS": "your-app-password"
235
+ }
48
236
  }
49
237
  }
50
238
  }
51
239
  }
52
240
  ```
53
241
 
242
+ > For more details about MCP configuration in VS Code, see the [official VS Code MCP documentation](https://code.visualstudio.com/docs/copilot/customization/mcp-servers).
243
+
54
244
  </details>
55
245
 
56
246
  <details>
57
247
  <summary>Cursor</summary>
58
248
 
59
- Add to your Cursor MCP settings:
249
+ Add to your Cursor MCP settings (`.cursor/mcp.json`):
60
250
 
61
251
  ```json
62
252
  {
63
253
  "mcpServers": {
64
254
  "mcp-mail-server": {
65
255
  "command": "npx",
66
- "args": ["mcp-mail-server"],
256
+ "args": ["-y", "mcp-mail-server"],
67
257
  "env": {
68
- "IMAP_HOST": "your-imap-server.com",
258
+ "IMAP_HOST": "imap.gmail.com",
69
259
  "IMAP_PORT": "993",
70
260
  "IMAP_SECURE": "true",
71
- "SMTP_HOST": "your-smtp-server.com",
261
+ "SMTP_HOST": "smtp.gmail.com",
72
262
  "SMTP_PORT": "465",
73
263
  "SMTP_SECURE": "true",
74
- "EMAIL_USER": "your-email@domain.com",
75
- "EMAIL_PASS": "your-password"
264
+ "EMAIL_USER": "your-email@gmail.com",
265
+ "EMAIL_PASS": "your-app-password"
76
266
  }
77
267
  }
78
268
  }
@@ -82,21 +272,54 @@ Add to your Cursor MCP settings:
82
272
  </details>
83
273
 
84
274
  <details>
85
- <summary>Other MCP Clients</summary>
275
+ <summary>Windsurf</summary>
86
276
 
87
- For global installation:
277
+ Add to your Windsurf MCP configuration file (`~/.codeium/windsurf/mcp_config.json`):
88
278
 
89
- ```bash
90
- npm install -g mcp-mail-server
279
+ ```json
280
+ {
281
+ "mcpServers": {
282
+ "mcp-mail-server": {
283
+ "command": "npx",
284
+ "args": ["-y", "mcp-mail-server"],
285
+ "env": {
286
+ "IMAP_HOST": "imap.gmail.com",
287
+ "IMAP_PORT": "993",
288
+ "IMAP_SECURE": "true",
289
+ "SMTP_HOST": "smtp.gmail.com",
290
+ "SMTP_PORT": "465",
291
+ "SMTP_SECURE": "true",
292
+ "EMAIL_USER": "your-email@gmail.com",
293
+ "EMAIL_PASS": "your-app-password"
294
+ }
295
+ }
296
+ }
297
+ }
91
298
  ```
92
299
 
93
- Then configure with:
300
+ </details>
301
+
302
+ <details>
303
+ <summary>Cline</summary>
304
+
305
+ Open Cline settings in VS Code, navigate to **MCP Servers**, click **Configure MCP Servers**, and add:
94
306
 
95
307
  ```json
96
308
  {
97
309
  "mcpServers": {
98
310
  "mcp-mail-server": {
99
- "command": "mcp-mail-server"
311
+ "command": "npx",
312
+ "args": ["-y", "mcp-mail-server"],
313
+ "env": {
314
+ "IMAP_HOST": "imap.gmail.com",
315
+ "IMAP_PORT": "993",
316
+ "IMAP_SECURE": "true",
317
+ "SMTP_HOST": "smtp.gmail.com",
318
+ "SMTP_PORT": "465",
319
+ "SMTP_SECURE": "true",
320
+ "EMAIL_USER": "your-email@gmail.com",
321
+ "EMAIL_PASS": "your-app-password"
322
+ }
100
323
  }
101
324
  }
102
325
  }
@@ -104,197 +327,202 @@ Then configure with:
104
327
 
105
328
  </details>
106
329
 
107
- ## Available Tools
108
-
109
- | Tool | Description |
110
- |------|-------------|
111
- | `connect_all` | Connect to both IMAP and SMTP servers |
112
- | `get_connection_status` | Check connection status and server info |
113
- | `disconnect_all` | Disconnect from all servers |
114
- | `open_mailbox` | Open specific mailbox/folder |
115
- | `list_mailboxes` | List available mail folders |
116
- | `search_messages` | Search emails with IMAP criteria |
117
- | `search_by_sender` | Find emails from specific sender |
118
- | `search_by_subject` | Search by subject keywords |
119
- | `search_by_body` | Search message content |
120
- | `search_since_date` | Find emails since date |
121
- | `search_unreplied_from_sender` | Find unreplied emails from specific sender |
122
- | `search_larger_than` | Find emails by size |
123
- | `get_message` | Retrieve email by UID |
124
- | `get_messages` | Retrieve multiple emails |
125
- | `delete_message` | Delete email by UID |
126
- | `get_unseen_messages` | Get all unread emails |
127
- | `get_recent_messages` | Get recent emails |
128
- | `send_email` | Send email via SMTP |
129
- | `reply_to_email` | Reply to specific email |
130
-
131
330
  <details>
132
- <summary>Detailed Tool Parameters</summary>
133
-
134
- ### Connection Management
135
- - **connect_all**: No parameters required
136
- - **get_connection_status**: No parameters required
137
- - **disconnect_all**: No parameters required
138
-
139
- ### Mailbox Operations
140
- - **open_mailbox**: `mailboxName` (string, default: "INBOX"), `readOnly` (boolean)
141
- - **list_mailboxes**: No parameters required
142
-
143
- ### Search Operations
144
- - **search_messages**: `criteria` (array, IMAP search criteria)
145
- - **search_by_sender**: `sender` (string, email address)
146
- - **search_by_subject**: `subject` (string, keywords)
147
- - **search_by_body**: `text` (string, search text)
148
- - **search_since_date**: `date` (string, date format)
149
- - **search_unreplied_from_sender**: `sender` (string, email address), `startDate` (string, optional), `endDate` (string, optional)
150
- - **search_larger_than**: `size` (number, bytes)
151
-
152
- ### Message Operations
153
- - **get_message**: `uid` (number), `markSeen` (boolean, optional)
154
- - **get_messages**: `uids` (array), `markSeen` (boolean, optional)
155
- - **delete_message**: `uid` (number)
156
-
157
- ### Email Sending
158
- - **send_email**: `to` (string), `subject` (string), `text` (string, optional), `html` (string, optional), `cc` (string, optional), `bcc` (string, optional)
159
- - **reply_to_email**: `originalUid` (number), `text` (string), `html` (string, optional), `replyToAll` (boolean, optional), `includeOriginal` (boolean, optional)
331
+ <summary>Cherry Studio</summary>
332
+
333
+ Open Cherry Studio settings, navigate to **MCP Servers**, click **Add Server**, select type as **STDIO**, then fill in:
334
+
335
+ - **Command**: `npx`
336
+ - **Args**: `-y mcp-mail-server`
337
+ - **Env Variables**: Add all required environment variables (see [Configuration](#configuration))
160
338
 
161
339
  </details>
162
340
 
341
+ <details>
342
+ <summary>Other MCP Clients (Augment Code, Trae, Zed, Amazon Q Developer, etc.)</summary>
343
+
344
+ Most MCP clients follow a similar JSON configuration pattern. The standard `mcpServers` config block works across nearly all clients:
345
+
346
+ ```json
347
+ {
348
+ "mcpServers": {
349
+ "mcp-mail-server": {
350
+ "command": "npx",
351
+ "args": ["-y", "mcp-mail-server"],
352
+ "env": {
353
+ "IMAP_HOST": "imap.gmail.com",
354
+ "IMAP_PORT": "993",
355
+ "IMAP_SECURE": "true",
356
+ "SMTP_HOST": "smtp.gmail.com",
357
+ "SMTP_PORT": "465",
358
+ "SMTP_SECURE": "true",
359
+ "EMAIL_USER": "your-email@gmail.com",
360
+ "EMAIL_PASS": "your-app-password"
361
+ }
362
+ }
363
+ }
364
+ }
365
+ ```
366
+
367
+ Just place it in the MCP configuration file for your specific client. Refer to your client's documentation for the exact file location.
368
+
369
+ Alternatively, install globally and use `mcp-mail-server` as the command directly:
370
+
371
+ ```bash
372
+ npm install -g mcp-mail-server
373
+ ```
374
+
375
+ </details>
163
376
 
164
377
  ## Usage Examples
165
378
 
166
379
  Use natural language commands with your AI assistant:
167
380
 
168
381
  ### Basic Operations
169
- - *"Connect to my email servers"*
170
- - *"Show me all unread emails"*
171
- - *"Search for emails from boss@company.com"*
172
- - *"Send an email to team@company.com about the meeting"*
173
- - *"Reply to email with UID 123"*
382
+
383
+ - _"Connect to my email servers"_
384
+ - _"Show me all unread emails"_
385
+ - _"Search for emails from boss@company.com"_
386
+ - _"Send an email to team@company.com about the meeting"_
387
+ - _"Reply to email with UID 123"_
174
388
 
175
389
  ### Advanced Searches
176
- - *"Find emails with 'urgent' in the subject from last week"*
177
- - *"Show me unreplied emails from boss@company.com"*
178
- - *"Show me large emails over 5MB"*
179
- - *"Get all emails from the Sales folder"*
180
390
 
181
- ### Email Management
182
- - *"Delete the email with UID 123"*
183
- - *"Mark recent emails as read"*
184
- - *"List all my email folders"*
391
+ - _"Find emails with 'urgent' in the subject from last week"_
392
+ - _"Show me unreplied emails from boss@company.com"_
393
+ - _"Show me large emails over 5MB"_
394
+ - _"Get all emails from the Sales folder"_
395
+
396
+ ### Attachment Operations
397
+
398
+ - _"Get message 123 with attachment content included"_
399
+ - _"Get messages 101 and 102 without attachment content"_
400
+ - _"Export the first attachment of message 123 to /tmp/report.pdf"_
401
+ - _"Export attachment named invoice.pdf from message 123 to /tmp/invoice.pdf"_
402
+
403
+ ### Email Management
404
+
405
+ - _"Delete the email with UID 123"_
406
+ - _"Mark recent emails as read"_
407
+ - _"List all my email folders"_
185
408
 
186
409
  ## Configuration
187
410
 
188
411
  ### Environment Variables
189
412
 
190
- **⚠️ All variables are required**
413
+ All environment variables are **required**:
191
414
 
192
- | Variable | Description | Example |
193
- |----------|-------------|---------|
194
- | `IMAP_HOST` | IMAP server address | `imap.gmail.com` |
195
- | `IMAP_PORT` | IMAP port number | `993` |
196
- | `IMAP_SECURE` | Enable TLS | `true` |
197
- | `SMTP_HOST` | SMTP server address | `smtp.gmail.com` |
198
- | `SMTP_PORT` | SMTP port number | `465` |
199
- | `SMTP_SECURE` | Enable SSL | `true` |
200
- | `EMAIL_USER` | Email username | `your-email@gmail.com` |
201
- | `EMAIL_PASS` | Email password/app password | `your-app-password` |
415
+ | Variable | Description | Example |
416
+ | ------------- | ------------------------------ | ---------------------- |
417
+ | `IMAP_HOST` | IMAP server address | `imap.gmail.com` |
418
+ | `IMAP_PORT` | IMAP port number | `993` |
419
+ | `IMAP_SECURE` | Enable TLS/SSL | `true` |
420
+ | `SMTP_HOST` | SMTP server address | `smtp.gmail.com` |
421
+ | `SMTP_PORT` | SMTP port number | `465` |
422
+ | `SMTP_SECURE` | Enable TLS/SSL | `true` |
423
+ | `EMAIL_USER` | Email username | `your-email@gmail.com` |
424
+ | `EMAIL_PASS` | Email password or app password | `your-app-password` |
202
425
 
203
- ### Common Email Providers
426
+ ### Provider-Specific Setup
204
427
 
205
428
  <details>
206
- <summary>Gmail Configuration</summary>
429
+ <summary>Gmail</summary>
207
430
 
208
- ```bash
431
+ ```
209
432
  IMAP_HOST=imap.gmail.com
210
433
  IMAP_PORT=993
211
434
  IMAP_SECURE=true
212
435
  SMTP_HOST=smtp.gmail.com
213
436
  SMTP_PORT=465
214
437
  SMTP_SECURE=true
215
- EMAIL_USER=your-email@gmail.com
216
- EMAIL_PASS=your-app-password
217
438
  ```
218
439
 
219
- **Note**: Use [App Passwords](https://support.google.com/accounts/answer/185833) instead of your regular password.
440
+ > [!IMPORTANT]
441
+ > Gmail requires an [App Password](https://support.google.com/accounts/answer/185833). Enable 2-Step Verification first, then generate an app-specific password.
220
442
 
221
443
  </details>
222
444
 
223
445
  <details>
224
- <summary>Outlook/Hotmail Configuration</summary>
446
+ <summary>Outlook / Hotmail</summary>
225
447
 
226
- ```bash
448
+ ```
227
449
  IMAP_HOST=outlook.office365.com
228
450
  IMAP_PORT=993
229
451
  IMAP_SECURE=true
230
452
  SMTP_HOST=smtp.office365.com
231
453
  SMTP_PORT=587
232
454
  SMTP_SECURE=true
233
- EMAIL_USER=your-email@outlook.com
234
- EMAIL_PASS=your-password
235
455
  ```
236
456
 
237
457
  </details>
238
458
 
239
- ### Security Notes
459
+ <details>
460
+ <summary>Yahoo Mail</summary>
461
+
462
+ ```
463
+ IMAP_HOST=imap.mail.yahoo.com
464
+ IMAP_PORT=993
465
+ IMAP_SECURE=true
466
+ SMTP_HOST=smtp.mail.yahoo.com
467
+ SMTP_PORT=465
468
+ SMTP_SECURE=true
469
+ ```
470
+
471
+ > [!IMPORTANT]
472
+ > Yahoo requires an [App Password](https://help.yahoo.com/kb/generate-manage-third-party-passwords-sln15241.html). Enable 2-Step Verification first.
473
+
474
+ </details>
475
+
476
+ ### Security Best Practices
477
+
478
+ - **Use App Passwords**: Always use app-specific passwords instead of your main password
479
+ - **Enable 2FA**: Enable two-factor authentication on your email account
480
+ - **Use TLS/SSL**: Always set `IMAP_SECURE=true` and `SMTP_SECURE=true`
481
+ - **Environment Variables Only**: Never hardcode credentials in configuration files
240
482
 
241
- - **Use App Passwords**: Enable 2FA and use app-specific passwords when available
242
- - **TLS/SSL Required**: Always use secure connections (IMAP_SECURE=true, SMTP_SECURE=true)
243
- - **Environment Variables**: Never hardcode credentials in configuration files
483
+ ## Debugging
484
+
485
+ You can use the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) to debug the server:
486
+
487
+ ```bash
488
+ npx @modelcontextprotocol/inspector npx mcp-mail-server
489
+ ```
490
+
491
+ Set the required environment variables in the Inspector's environment configuration panel before connecting.
244
492
 
245
493
  ## Development
246
494
 
247
- <details>
248
- <summary>Local Development Setup</summary>
495
+ 1. Clone the repository:
249
496
 
250
- 1. **Clone the repository**:
251
497
  ```bash
252
498
  git clone https://github.com/yunfeizhu/mcp-mail-server.git
253
499
  cd mcp-mail-server
254
500
  ```
255
501
 
256
- 2. **Install dependencies**:
502
+ 2. Install dependencies:
503
+
257
504
  ```bash
258
505
  npm install
259
506
  ```
260
507
 
261
- 3. **Build the project**:
262
- ```bash
263
- npm run build
264
- ```
508
+ 3. Build the project:
265
509
 
266
- 4. **Set environment variables**:
267
510
  ```bash
268
- export IMAP_HOST=your-imap-server.com
269
- export IMAP_PORT=993
270
- export IMAP_SECURE=true
271
- export SMTP_HOST=your-smtp-server.com
272
- export SMTP_PORT=465
273
- export SMTP_SECURE=true
274
- export EMAIL_USER=your-email@domain.com
275
- export EMAIL_PASS=your-password
511
+ npm run build
276
512
  ```
277
513
 
278
- 5. **Run the server**:
514
+ 4. Run tests:
279
515
  ```bash
280
- npm start
516
+ npm test
281
517
  ```
282
518
 
283
- </details>
284
-
285
519
  ## Contributing
286
520
 
287
- Contributions are welcome! Please feel free to submit a Pull Request.
521
+ Contributions are welcome! Whether you want to add new tools, enhance existing functionality, or improve documentation — pull requests are appreciated.
288
522
 
289
- ## License
523
+ For examples of other MCP servers and implementation patterns, see:
524
+ https://github.com/modelcontextprotocol/servers
290
525
 
291
- MIT License - see [LICENSE](LICENSE) file for details.
292
-
293
- ---
294
-
295
- **Package Information:**
296
- - Package: `mcp-mail-server`
297
- - Node.js: ≥18.0.0
298
- - Repository: [GitHub](https://github.com/yunfeizhu/mcp-mail-server)
299
- - Issues: [Report bugs](https://github.com/yunfeizhu/mcp-mail-server/issues)
526
+ ## License
300
527
 
528
+ This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the [LICENSE](LICENSE) file in the project repository.
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{Server as e}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as t}from"@modelcontextprotocol/sdk/server/stdio.js";import{ListToolsRequestSchema as r,CallToolRequestSchema as n}from"@modelcontextprotocol/sdk/types.js";import s from"imap";import{EventEmitter as o}from"events";import{simpleParser as i}from"mailparser";import a from"nodemailer";class IMAPClient extends o{imap=null;config;connected=!1;authenticated=!1;currentBox=null;constructor(e){super(),this.config=e}async connect(){return new Promise((e,t)=>{console.error(`[IMAP] Connecting to ${this.config.host}:${this.config.port} (TLS: ${this.config.tls})`);const r={user:this.config.username,password:this.config.password,host:this.config.host,port:this.config.port,tls:this.config.tls||!1,connTimeout:this.config.connTimeout||6e4,authTimeout:this.config.authTimeout||3e4,keepalive:!1!==this.config.keepalive};this.imap=new s(r),this.imap.once("ready",async()=>{console.error("[IMAP] Connection ready"),this.connected=!0,this.authenticated=!0;try{await this.openBox("INBOX",!0),console.error("[IMAP] Auto-opened INBOX")}catch(e){console.error("[IMAP] Failed to auto-open INBOX:",e instanceof Error?e.message:String(e))}e()}),this.imap.once("error",e=>{console.error("[IMAP] Connection error:",e.message),t(new Error(`IMAP connection failed: ${e.message}`))}),this.imap.once("end",()=>{console.error("[IMAP] Connection ended"),this.connected=!1,this.authenticated=!1,this.currentBox=null}),this.imap.connect()})}async openBox(e="INBOX",t=!1){if(!this.imap||!this.authenticated)throw new Error("Not connected or authenticated");return new Promise((r,n)=>{this.imap.openBox(e,t,(t,s)=>{if(t)return console.error(`[IMAP] Failed to open box ${e}:`,t.message),void n(new Error(`Failed to open mailbox: ${t.message}`));console.error(`[IMAP] Opened box ${e}`),this.currentBox=e;const o={name:e,messages:{total:s.messages.total,new:s.messages.new,unseen:s.messages.unseen},permFlags:s.permFlags,uidvalidity:s.uidvalidity,uidnext:s.uidnext};r(o)})})}async getBoxes(){if(!this.imap||!this.authenticated)throw new Error("Not connected or authenticated");return new Promise((e,t)=>{this.imap.getBoxes((r,n)=>{r?t(new Error(`Failed to get boxes: ${r.message}`)):e(n)})})}async search(e=["ALL"]){if(!this.imap)throw new Error("Not connected to IMAP server");return this.currentBox||await this.openBox("INBOX",!0),new Promise((t,r)=>{this.imap.search([e],(e,n)=>{if(e)return console.error("[IMAP] Search failed:",e.message),void r(new Error(`Search failed: ${e.message}`));console.error(`[IMAP] Search found ${n.length} messages`),t(n)})})}async fetchMessages(e,t={}){if(!this.imap)throw new Error("Not connected to IMAP server");this.currentBox||await this.openBox("INBOX",!0);const r={bodies:t.bodies||["HEADER","TEXT"],struct:!1!==t.struct,envelope:!1!==t.envelope,markSeen:t.markSeen||!1,...t};return new Promise((t,n)=>{const s=[],o=new Map;if(0===e.length)return void t(s);const a=this.imap.fetch(e,r);a.on("message",(e,t)=>{console.error(`[IMAP] Processing message ${t}`);let r={},n="";const s=[],i={uid:0,id:t,flags:[],date:"",size:0};e.on("body",(e,t)=>{const o=[];e.on("data",e=>{o.push(e),s.push(e)}),e.once("end",()=>{const e=Buffer.concat(o);if("HEADER"===t.which){const t=e.toString("utf8");r=this.parseHeaders(t)}else"TEXT"===t.which&&(n=e.toString("utf8"))})}),e.once("attributes",e=>{i.uid=e.uid,i.flags=e.flags||[];const t=e.date||new Date;i.date=t.toLocaleString("zh-CN",{timeZone:"Asia/Shanghai",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"}),i.size=e.size||0}),e.once("end",()=>{console.error(`[IMAP] Message ${t} processed, preparing for parse`),o.set(t,{message:i,headers:r,body:n,rawBuffer:Buffer.concat(s)})})}),a.once("error",e=>{console.error("[IMAP] Fetch error:",e.message),n(new Error(`Fetch failed: ${e.message}`))}),a.once("end",async()=>{console.error(`[IMAP] Fetch completed, parsing ${o.size} messages`);for(const[e,t]of o)try{const e=await i(t.rawBuffer),r=e=>e?Array.isArray(e)?e.map(e=>n(e)).filter(Boolean).join(", "):n(e):"",n=e=>{if(!e)return"";if("string"==typeof e){const t=e.match(/<([^>]+)>/)||e.match(/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/);return t?t[1]:e}if(e&&"object"==typeof e){if(e.address)return e.address;if(e.text){const t=e.text.match(/<([^>]+)>/)||e.text.match(/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/);return t?t[1]:e.text}}return""};s.push({...t.message,subject:e.subject||"No Subject",from:r(e.from),to:r(e.to),cc:r(e.cc)||void 0,bcc:r(e.bcc)||void 0,text:e.text,html:e.html})}catch(r){console.error(`[IMAP] Failed to parse message ${e}:`,r),s.push({...t.message,subject:t.headers.subject||"Parse Failed",from:t.headers.from||"",to:t.headers.to||"",cc:t.headers.cc||void 0,bcc:t.headers.bcc||void 0,text:t.body.trim()})}console.error(`[IMAP] All messages parsed, returning ${s.length} messages`),t(s)})})}async getMessage(e){const t=await this.fetchMessages([e]);if(0===t.length)throw new Error(`Message with UID ${e} not found`);return t[0]}async deleteMessage(e){if(!this.imap)throw new Error("Not connected to IMAP server");return this.currentBox||await this.openBox("INBOX",!1),new Promise((t,r)=>{this.imap.addFlags(e,["\\Deleted"],n=>{if(n)return console.error(`[IMAP] Failed to mark message ${e} as deleted:`,n.message),void r(new Error(`Failed to delete message: ${n.message}`));console.error(`[IMAP] Message ${e} marked for deletion`),this.imap.expunge(n=>{if(n)return console.error("[IMAP] Failed to expunge:",n.message),void r(new Error(`Failed to expunge deleted messages: ${n.message}`));console.error(`[IMAP] Message ${e} deleted successfully`),t()})})})}async getMessageCount(){return this.currentBox||await this.openBox("INBOX",!0),(await this.search(["ALL"])).length}async getUnseenMessages(){const e=await this.search(["UNSEEN"]);return this.fetchMessages(e)}async getRecentMessages(){const e=await this.search(["RECENT"]);return this.fetchMessages(e)}parseHeaders(e){const t={},r=e.split("\r\n");let n="",s="";for(const e of r)if(e.match(/^\s/)&&n)s+=" "+e.trim();else{n&&(t[n.toLowerCase()]=s.trim());const r=e.indexOf(":");r>-1?(n=e.substring(0,r).trim(),s=e.substring(r+1).trim()):(n="",s="")}return n&&(t[n.toLowerCase()]=s.trim()),t}async disconnect(){if(this.imap)return this.connected?new Promise(e=>{const t=setTimeout(()=>{console.error("[IMAP] Disconnect timeout, forcing cleanup"),this.connected=!1,this.authenticated=!1,this.currentBox=null,this.imap=null,e()},5e3);this.imap.once("end",()=>{clearTimeout(t),console.error("[IMAP] Disconnected"),this.connected=!1,this.authenticated=!1,this.currentBox=null,this.imap=null,e()}),this.imap.once("error",r=>{clearTimeout(t),console.error("[IMAP] Disconnect error:",r.message),this.connected=!1,this.authenticated=!1,this.currentBox=null,this.imap=null,e()});try{this.imap.end()}catch(r){clearTimeout(t),console.error("[IMAP] Error calling end():",r),this.connected=!1,this.authenticated=!1,this.currentBox=null,this.imap=null,e()}}):(this.imap=null,this.authenticated=!1,void(this.currentBox=null))}isConnected(){return this.connected&&this.authenticated}getCurrentBox(){return this.currentBox}getCurrentUsername(){return this.config?.username||null}async saveMessageToFolder(e,t="INBOX.Sent"){if(!this.connected)throw new Error("IMAP client is not connected");return new Promise((r,n)=>{this.imap.openBox(t,!1,s=>{s?(console.warn(`[IMAP] Folder ${t} not found, trying to create it`),this.imap.addBox(t,s=>{if(s)return console.error(`[IMAP] Failed to create folder ${t}:`,s.message),void this.trySaveToAlternateSentFolders(e,r,n);this.saveToOpenedFolder(e,t,r,n)})):this.saveToOpenedFolder(e,t,r,n)})})}saveToOpenedFolder(e,t,r,n){this.imap.append(e,{mailbox:t},e=>{e?(console.error(`[IMAP] Failed to save message to ${t}:`,e.message),n(new Error(`Failed to save message to ${t}: ${e.message}`))):r()})}trySaveToAlternateSentFolders(e,t,r){const n=["Sent","SENT","Sent Items","Sent Messages","已发送"];let s=0;const o=()=>{if(s>=n.length)return console.warn("[IMAP] All sent folder attempts failed, message not saved to sent folder"),void t();const i=n[s++];this.imap.openBox(i,!1,n=>{n?o():this.saveToOpenedFolder(e,i,t,r)})};o()}}class SMTPClient{transporter=null;config;constructor(e){this.config=e}async connect(){this.transporter=a.createTransport({host:this.config.host,port:this.config.port,secure:this.config.secure||!1,auth:{user:this.config.username,pass:this.config.password}});try{this.transporter&&await this.transporter.verify()}catch(e){throw new Error(`SMTP connection failed: ${e instanceof Error?e.message:String(e)}`)}}async sendMail(e){if(!this.transporter)throw new Error("SMTP client not connected");const t={from:e.from||this.config.username,to:Array.isArray(e.to)?e.to.join(", "):e.to,cc:e.cc?Array.isArray(e.cc)?e.cc.join(", "):e.cc:void 0,bcc:e.bcc?Array.isArray(e.bcc)?e.bcc.join(", "):e.bcc:void 0,subject:e.subject,text:e.text,html:e.html,attachments:e.attachments};try{const e=await this.transporter.sendMail(t);return{messageId:e.messageId,response:e.response,accepted:e.accepted||[],rejected:e.rejected||[]}}catch(e){throw new Error(`Failed to send email: ${e instanceof Error?e.message:String(e)}`)}}getCurrentUsername(){return this.config?.username||null}isConnected(){return null!==this.transporter}async disconnect(){if(this.transporter)try{this.transporter.close(),console.error("[SMTP] Disconnected successfully")}catch(e){console.error("[SMTP] Error during disconnect:",e instanceof Error?e.message:String(e))}finally{this.transporter=null}}}function c(e,t){const r=process.env[e];if(!r)throw new Error(`Missing required environment variable: ${e}. Please set this variable in your MCP server configuration.`);return r}function l(e){const t=process.env[e];if(!t)throw new Error(`Missing required environment variable: ${e}. Please set this variable to 'true' or 'false' in your MCP server configuration.`);if("true"!==t.toLowerCase()&&"false"!==t.toLowerCase())throw new Error(`Invalid boolean value for environment variable ${e}: ${t}. Must be 'true' or 'false'.`);return"true"===t.toLowerCase()}function d(e){const t=process.env[e];if(!t)throw new Error(`Missing required environment variable: ${e}. Please set this variable to a valid number in your MCP server configuration.`);const r=parseInt(t,10);if(isNaN(r))throw new Error(`Invalid number value for environment variable ${e}: ${t}. Must be a valid number.`);return r}const h={IMAP:{host:c("IMAP_HOST"),port:d("IMAP_PORT"),username:c("EMAIL_USER"),password:c("EMAIL_PASS"),tls:l("IMAP_SECURE")},SMTP:{host:c("SMTP_HOST"),port:d("SMTP_PORT"),username:c("EMAIL_USER"),password:c("EMAIL_PASS"),secure:l("SMTP_SECURE")}},m=["INBOX.Sent","Sent","SENT","Sent Items","Sent Messages","已发送"],u=["INBOX",...m];(new class{server;imapClient=null;smtpClient=null;isInitializing=!1;formatError(e,t){return`${t}: ${e instanceof Error?e.message:String(e)}`}isDateOnly(e){return[/^\d{4}-\d{2}-\d{2}$/,/^\d{2}-\w{3}-\d{4}$/,/^\w{3}\s+\d{1,2},?\s+\d{4}$/].some(t=>t.test(e.trim()))}filterMessagesByDateRange(e,t,r){if(!t&&!r)return e;let n=null,s=null;t&&(n=new Date(t),isNaN(n.getTime())?(console.error(`[Filter] Invalid start date format: ${t}, skipping start date filter`),n=null):console.error(`[Filter] Start date parsed as: ${n.toISOString()}`)),r&&(s=new Date(r),isNaN(s.getTime())?(console.error(`[Filter] Invalid end date format: ${r}, skipping end date filter`),s=null):this.isDateOnly(r)?(s.setHours(23,59,59,999),console.error(`[Filter] End date adjusted to end of day: ${s.toISOString()}`)):console.error(`[Filter] End date parsed as: ${s.toISOString()}`));const o=e.length,i=e.filter(e=>{if(!e.date)return!0;const t=new Date(e.date);return!(!isNaN(t.getTime())&&(n&&t<n||s&&t>s))});return console.error(`[Filter] Date filtering: ${o} -> ${i.length} messages`),i}async searchInMultipleMailboxes(e,t,r,n="",s="",o){const i=["INBOX"];let a=!1;for(const e of m)try{await this.imapClient.openBox(e,!0),i.push(e),a=!0;break}catch(t){console.error(`[IMAP] Failed to open sent mailbox ${e}: ${t instanceof Error?t.message:String(t)}`)}const c={searchType:t,searchValue:r,searchCriteria:e,mailboxesSearched:[],totalMatches:0,messages:[]};for(const t of i)try{console.error(`[IMAP] Searching in mailbox: ${t}`),await this.imapClient.openBox(t,!0);const r=await this.imapClient.search(e);console.error(`[IMAP] Found ${r.length} messages in ${t}`);let i=[],a=[];if(r.length>0){const e=o?r.slice(0,o):r;console.error(`[IMAP] Auto-fetching content for ${e.length} messages from ${t}${o?` (limited from ${r.length})`:""}`);let l=(await this.imapClient.fetchMessages(e)).map(e=>({...e,sourceMailbox:t}));(n||s)&&(l=this.filterMessagesByDateRange(l,n,s)),i=l,a=l.map(e=>e.uid),c.messages.push(...i)}const l={mailbox:t,matchingUIDs:a,messageCount:i.length};c.mailboxesSearched.push(l)}catch(e){console.error(`[IMAP] Error searching in ${t}:`,e),c.mailboxesSearched.push({mailbox:t,error:`Failed to search: ${e instanceof Error?e.message:String(e)}`,matchingUIDs:[],messageCount:0})}if(c.totalMatches=c.messages.length,c.messages.sort((e,t)=>{const r=new Date(e.date||0);return new Date(t.date||0).getTime()-r.getTime()}),c.totalMatches>0){let e=`Found and retrieved ${c.totalMatches} messages across ${c.mailboxesSearched.length} mailboxes`;(n||s)&&(e+=" (filtered by date range)"),c.note=e}else c.note="No messages found in any of the searched mailboxes";return a||(c.warning="Could not find sent mailbox - only searched INBOX"),c}constructor(){this.validateConfig(),this.server=new e({name:"mcp-mail",version:"1.0.0"},{capabilities:{tools:{}}}),this.setupToolHandlers(),this.setupErrorHandling()}setupErrorHandling(){this.server.onerror=e=>console.error("[MCP Error]",e),process.on("SIGINT",async()=>{this.imapClient&&await this.imapClient.disconnect(),this.smtpClient&&await this.smtpClient.disconnect(),await this.server.close(),process.exit(0)})}setupToolHandlers(){this.server.setRequestHandler(r,async()=>({tools:[{name:"connect_all",description:"Connect to both IMAP and SMTP servers simultaneously",inputSchema:{type:"object",properties:{}}},{name:"list_mailboxes",description:"List all available mailboxes (folders). Auto-connects if not already connected.",inputSchema:{type:"object",properties:{}}},{name:"open_mailbox",description:"Open a specific mailbox (folder) and optionally retrieve sent mailbox info. Due to IMAP protocol limitations, only one mailbox stays open. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{mailboxName:{type:"string",description:"Name of the mailbox to open (default: INBOX)",default:"INBOX"},readOnly:{type:"boolean",description:"Open mailbox in read-only mode (default: false)",default:!1},openSent:{type:"boolean",description:"Also retrieve sent mailbox information (default: true)",default:!0}}}},{name:"get_message_count",description:"Get the total number of messages in current mailbox. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{}}},{name:"get_unseen_messages",description:"Get all unseen (unread) messages. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{}}},{name:"get_recent_messages",description:"Get all recent messages. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{}}},{name:"search_by_sender",description:"Search messages from a specific sender with optional date range. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{sender:{type:"string",description:"Email address of the sender to search for"},startDate:{type:"string",description:'Optional start date/time for filtering. Supports multiple formats: "2025-07-01", "2025-07-01 14:30:00", "01-Jul-2025", or ISO format. Leave empty to not filter by start date.'},endDate:{type:"string",description:'Optional end date/time for filtering. Supports multiple formats: "2025-08-01", "2025-08-01 23:59:59", "01-Aug-2025", or ISO format. Leave empty to not filter by end date.'}},required:["sender"]}},{name:"search_by_subject",description:"Search messages by subject keywords with optional date range. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{subject:{type:"string",description:"Keywords to search in email subject"},startDate:{type:"string",description:'Optional start date/time for filtering. Supports multiple formats: "2025-07-01", "2025-07-01 14:30:00", "01-Jul-2025", or ISO format. Leave empty to not filter by start date.'},endDate:{type:"string",description:'Optional end date/time for filtering. Supports multiple formats: "2025-08-01", "2025-08-01 23:59:59", "01-Aug-2025", or ISO format. Leave empty to not filter by end date.'}},required:["subject"]}},{name:"search_by_recipient",description:"Search messages sent to a specific recipient email address with optional date range. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{recipient:{type:"string",description:"Email address of the recipient to search for"},startDate:{type:"string",description:'Optional start date/time for filtering. Supports multiple formats: "2025-07-01", "2025-07-01 14:30:00", "01-Jul-2025", or ISO format. Leave empty to not filter by start date.'},endDate:{type:"string",description:'Optional end date/time for filtering. Supports multiple formats: "2025-08-01", "2025-08-01 23:59:59", "01-Aug-2025", or ISO format. Leave empty to not filter by end date.'}},required:["recipient"]}},{name:"search_since_date",description:"Search messages from a specific date until now (not for date ranges). Use search_messages for complex date ranges.",inputSchema:{type:"object",properties:{date:{type:"string",description:'Start date to search from (searches from this date to present). Formats: "April 20, 2010", "20-Apr-2010", or "2010-04-20"'}},required:["date"]}},{name:"search_unread_from_sender",description:"Search unread messages from a specific sender with optional date range (demonstrates AND logic). Auto-connects if not already connected.",inputSchema:{type:"object",properties:{sender:{type:"string",description:"Email address of the sender"},startDate:{type:"string",description:'Optional start date/time for filtering. Supports multiple formats: "2025-07-01", "2025-07-01 14:30:00", "01-Jul-2025", or ISO format. Leave empty to not filter by start date.'},endDate:{type:"string",description:'Optional end date/time for filtering. Supports multiple formats: "2025-08-01", "2025-08-01 23:59:59", "01-Aug-2025", or ISO format. Leave empty to not filter by end date.'}},required:["sender"]}},{name:"search_unreplied_from_sender",description:"Search unreplied messages from a specific sender with optional date range. Identifies messages that have not been replied to by checking for corresponding reply messages. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{sender:{type:"string",description:"Email address of the sender to search for unreplied messages"},startDate:{type:"string",description:'Optional start date/time for filtering. Supports multiple formats: "2025-07-01", "2025-07-01 14:30:00", "01-Jul-2025", or ISO format. Leave empty to not filter by start date.'},endDate:{type:"string",description:'Optional end date/time for filtering. Supports multiple formats: "2025-08-01", "2025-08-01 23:59:59", "01-Aug-2025", or ISO format. Leave empty to not filter by end date.'},limit:{type:"number",description:"Maximum number of messages to process from each search (default: 10, maximum: 200). Since unreplied emails are typically few, smaller limits are recommended.",default:10}},required:["sender"]}},{name:"search_by_body",description:"Search messages containing specific text in the body with optional date range. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{text:{type:"string",description:"Text to search for in message body"},startDate:{type:"string",description:'Optional start date/time for filtering. Supports multiple formats: "2025-07-01", "2025-07-01 14:30:00", "01-Jul-2025", or ISO format. Leave empty to not filter by start date.'},endDate:{type:"string",description:'Optional end date/time for filtering. Supports multiple formats: "2025-08-01", "2025-08-01 23:59:59", "01-Aug-2025", or ISO format. Leave empty to not filter by end date.'}},required:["text"]}},{name:"search_with_keyword",description:"Search messages with specific keyword/flag with optional date range. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{keyword:{type:"string",description:"Keyword to search for"},startDate:{type:"string",description:'Optional start date/time for filtering. Supports multiple formats: "2025-07-01", "2025-07-01 14:30:00", "01-Jul-2025", or ISO format. Leave empty to not filter by start date.'},endDate:{type:"string",description:'Optional end date/time for filtering. Supports multiple formats: "2025-08-01", "2025-08-01 23:59:59", "01-Aug-2025", or ISO format. Leave empty to not filter by end date.'}},required:["keyword"]}},{name:"get_messages",description:"Retrieve multiple messages by their UIDs. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{uids:{type:"array",description:"Array of message UIDs to retrieve",items:{type:"number"}},markSeen:{type:"boolean",description:"Mark messages as seen when retrieving (default: false)",default:!1}},required:["uids"]}},{name:"get_message",description:"Retrieve a specific email message by UID. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{uid:{type:"number",description:"Message UID to retrieve"},markSeen:{type:"boolean",description:"Mark message as seen when retrieving (default: false)",default:!1}},required:["uid"]}},{name:"send_email",description:"Send an email via SMTP. Auto-connects to SMTP server if not already connected.",inputSchema:{type:"object",properties:{to:{type:"string",description:"Recipient email address(es), comma-separated"},subject:{type:"string",description:"Email subject"},text:{type:"string",description:"Plain text email body"},html:{type:"string",description:"HTML email body (optional)"},cc:{type:"string",description:"CC recipients, comma-separated (optional)"},bcc:{type:"string",description:"BCC recipients, comma-separated (optional)"}},required:["to","subject"]}},{name:"reply_to_email",description:"Reply to a specific email by UID. Automatically sets reply headers, adds Re: prefix, and includes original message. Auto-connects to both IMAP and SMTP if not already connected.",inputSchema:{type:"object",properties:{originalUid:{type:"number",description:"UID of the original message to reply to"},text:{type:"string",description:"Reply message text"},html:{type:"string",description:"Reply message HTML (optional)"},replyToAll:{type:"boolean",description:"Reply to all recipients instead of just sender (default: false)",default:!1},includeOriginal:{type:"boolean",description:"Include original message in reply (default: true)",default:!0}},required:["originalUid"]}},{name:"delete_message",description:"Delete a specific email message by UID. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{uid:{type:"number",description:"Message UID to delete"}},required:["uid"]}},{name:"get_connection_status",description:"Check the current connection status of both IMAP and SMTP servers.",inputSchema:{type:"object",properties:{}}},{name:"disconnect_all",description:"Disconnect from both IMAP and SMTP servers. Only disconnects if currently connected.",inputSchema:{type:"object",properties:{}}}]})),this.server.setRequestHandler(n,async e=>{const{name:t,arguments:r}=e.params;try{switch(t){case"open_mailbox":return await this.handleOpenMailbox(r||{});case"list_mailboxes":return await this.handleListMailboxes();case"search_by_sender":return await this.handleSearchBySender(r||{});case"search_by_subject":return await this.handleSearchBySubject(r);case"search_by_recipient":return await this.handleSearchByRecipient(r);case"search_since_date":return await this.handleSearchSinceDate(r);case"search_unread_from_sender":return await this.handleSearchUnreadFromSender(r);case"search_unreplied_from_sender":return await this.handleSearchUnrepliedFromSender(r);case"search_by_body":return await this.handleSearchByBody(r);case"search_with_keyword":return await this.handleSearchWithKeyword(r);case"get_messages":return await this.handleGetMessages(r);case"get_message":return await this.handleGetMessage(r);case"delete_message":return await this.handleDeleteMessage(r);case"get_message_count":return await this.handleGetMessageCount();case"get_unseen_messages":return await this.handleGetUnseenMessages();case"get_recent_messages":return await this.handleGetRecentMessages();case"get_connection_status":return await this.handleGetConnectionStatus();case"send_email":return await this.handleSendEmail(r);case"reply_to_email":return await this.handleReplyToEmail(r);case"connect_all":return await this.handleConnectAll();case"disconnect_all":return await this.handleDisconnectAll();default:throw new Error(`Unknown tool: ${t}`)}}catch(e){return{content:[{type:"text",text:`Error: ${e instanceof Error?e.message:String(e)}`}]}}})}async ensureIMAPConnection(){if(!this.imapClient||!this.imapClient.isConnected())if(this.isInitializing)for(;this.isInitializing;)await new Promise(e=>setTimeout(e,100));else{this.isInitializing=!0;try{const e=h.IMAP;console.error(`[IMAP] Auto-connecting to ${e.host}:${e.port}`),this.imapClient=new IMAPClient(e),await this.imapClient.connect(),console.error("[IMAP] Auto-connection successful")}finally{this.isInitializing=!1}}}async ensureSMTPConnection(){if(this.smtpClient)return;const e=h.SMTP;console.error(`[SMTP] Auto-connecting to ${e.host}:${e.port}`),this.smtpClient=new SMTPClient(e),await this.smtpClient.connect(),console.error("[SMTP] Auto-connection successful")}async ensureRequiredConnections(e=!1,t=!1){e&&await this.ensureIMAPConnection(),t&&await this.ensureSMTPConnection()}async handleOpenMailbox(e){await this.ensureRequiredConnections(!0,!1);const t=e.mailboxName||"INBOX",r=e.readOnly||!1,n=!1!==e.openSent;try{const e={},s=await this.imapClient.openBox(t,r);if(e[t]=s,e.currentlyOpen=t,n&&"INBOX.Sent"!==t&&"Sent"!==t&&"SENT"!==t)try{const n=["INBOX.Sent","Sent","SENT","Sent Items","Sent Messages","已发送"];let s=!1;for(const o of n)try{const n=await this.imapClient.openBox(o,!0);e[o]=n,s=!0,await this.imapClient.openBox(t,r),e.currentlyOpen=t,e.note=`Retrieved info from both ${t} and ${o}. Currently open: ${t}`;break}catch(e){console.error(`[IMAP] Failed to open sent mailbox ${o}: ${e instanceof Error?e.message:String(e)}`)}s||(e.sentBoxWarning="Could not find any sent mailbox")}catch(t){e.sentBoxError=`Failed to access sent mailbox: ${t instanceof Error?t.message:String(t)}`}return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Failed to open mailbox"))}}async handleListMailboxes(){await this.ensureRequiredConnections(!0,!1);try{const e=await this.imapClient.getBoxes(),t=(e,r="",n=new Set)=>{if(n.has(e))return{name:r,circular:!0};n.add(e);const s={name:r,attribs:e.attribs||[],delimiter:e.delimiter||".",selectable:!e.attribs?.includes("\\Noselect")};if(e.children&&Object.keys(e.children).length>0){s.children={};for(const[o,i]of Object.entries(e.children)){const a=r?`${r}${e.delimiter||"."}${o}`:o;s.children[o]=t(i,a,new Set(n))}}return n.delete(e),s},r={};for(const[n,s]of Object.entries(e))r[n]=t(s,n);return{content:[{type:"text",text:JSON.stringify(r,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Failed to list mailboxes"))}}async handleSearchBySender(e){await this.ensureRequiredConnections(!0,!1);const t=e.sender,r=e.startDate||"",n=e.endDate||"";if(!t)throw new Error("sender parameter is required");try{const e=["FROM",t];console.error("[IMAP] Searching messages from sender across all mailboxes:",t);const s=await this.searchInMultipleMailboxes(e,"By Sender",t,r,n);return s.sender=t,r&&(s.startDate=r),n&&(s.endDate=n),{content:[{type:"text",text:JSON.stringify(s,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Search by sender failed"))}}async handleSearchBySubject(e){await this.ensureRequiredConnections(!0,!1);const t=e.subject,r=e.startDate||"",n=e.endDate||"";if(!t)throw new Error("subject parameter is required");try{const e=["SUBJECT",t];console.error("[IMAP] Searching messages with subject across all mailboxes:",t);const s=await this.searchInMultipleMailboxes(e,"By Subject",t,r,n);return s.subjectKeywords=t,r&&(s.startDate=r),n&&(s.endDate=n),{content:[{type:"text",text:JSON.stringify(s,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Search by subject failed"))}}async handleSearchByRecipient(e){await this.ensureRequiredConnections(!0,!1);const t=e.recipient,r=e.startDate||"",n=e.endDate||"";if(!t)throw new Error("recipient parameter is required");try{const e=["TO",t];console.error("[IMAP] Searching messages to recipient across all mailboxes:",t);const s=await this.searchInMultipleMailboxes(e,"By Recipient",t,r,n);return s.recipient=t,r&&(s.startDate=r),n&&(s.endDate=n),{content:[{type:"text",text:JSON.stringify(s,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Search by recipient failed"))}}async handleSearchSinceDate(e){await this.ensureRequiredConnections(!0,!1);const t=e.date;if(!t)throw new Error("date parameter is required");try{const e=["SINCE",t];console.error("[IMAP] Searching messages since date across all mailboxes:",t);const r=await this.searchInMultipleMailboxes(e,"Since Date",t);return r.sinceDate=t,r.note='Date format should be like "April 20, 2010" or "20-Apr-2010". Searched across multiple mailboxes.',{content:[{type:"text",text:JSON.stringify(r,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Search since date failed"))}}async handleSearchUnreadFromSender(e){await this.ensureRequiredConnections(!0,!1);const t=e.sender,r=e.startDate||"",n=e.endDate||"";if(!t)throw new Error("sender parameter is required");try{const e=["UNSEEN",["FROM",t]];console.error("[IMAP] Searching unread messages from sender across all mailboxes:",t);const s=await this.searchInMultipleMailboxes(e,"Unread messages from specific sender",t,r,n);return s.sender=t,r&&(s.startDate=r),n&&(s.endDate=n),s.note="By default, all criteria are ANDed together - finds messages that are BOTH unread AND from the specified sender. Searched across multiple mailboxes.",{content:[{type:"text",text:JSON.stringify(s,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Search unread from sender failed"))}}async handleSearchUnrepliedFromSender(e){await this.ensureRequiredConnections(!0,!1);const t=e.sender,r=e.startDate||"",n=e.endDate||"",s=Math.min(Math.max(e.limit||10,1),200);if(!t)throw new Error("sender parameter is required");try{console.error(`[IMAP] Searching unreplied messages from sender: ${t}`);const e=await this.searchInMultipleMailboxes(["FROM",t],"From Sender",t,r,n,s),o=await this.searchInMultipleMailboxes(["TO",t],"To Sender",t,r,n,s);console.error(`[IMAP] Found ${e.messages.length} messages from sender, ${o.messages.length} messages to sender`),e.messages.length>s&&(console.error(`[IMAP] Warning: Found ${e.messages.length} messages from sender, processing only the latest ${s}`),e.messages=e.messages.sort((e,t)=>new Date(t.date).getTime()-new Date(e.date).getTime()).slice(0,s));const i=this.detectUnrepliedMessagesAdvanced(e.messages,o.messages,t,s);console.error(`[IMAP] Found ${i.length} unreplied messages using advanced detection`);const a={searchType:"Unreplied messages from sender (Advanced)",searchValue:t,searchCriteria:["FROM",t],mailboxesSearched:e.mailboxesSearched,totalMatches:i.length,messages:i,sender:t,note:`Found ${i.length} unreplied messages from ${t} using advanced thread-aware detection.${s<200?` (limited to ${s} messages per search)`:""}`};return r&&(a.startDate=r),n&&(a.endDate=n),(r||n)&&(a.note+=" (filtered by date range)"),{content:[{type:"text",text:JSON.stringify(a,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Search unreplied from sender failed"))}}async handleSearchByBody(e){await this.ensureRequiredConnections(!0,!1);const t=e.text,r=e.startDate||"",n=e.endDate||"";if(!t)throw new Error("text parameter is required");try{const e=["BODY",t];console.error("[IMAP] Searching messages with body text across all mailboxes:",t);const s=await this.searchInMultipleMailboxes(e,"By Body Text",t,r,n);return s.bodyText=t,r&&(s.startDate=r),n&&(s.endDate=n),{content:[{type:"text",text:JSON.stringify(s,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Search by body failed"))}}async handleSearchWithKeyword(e){await this.ensureRequiredConnections(!0,!1);const t=e.keyword,r=e.startDate||"",n=e.endDate||"";if(!t)throw new Error("keyword parameter is required");try{const e=["KEYWORD",t];console.error("[IMAP] Searching messages with keyword across all mailboxes:",t);const s=await this.searchInMultipleMailboxes(e,"With Keyword",t,r,n);return s.keyword=t,r&&(s.startDate=r),n&&(s.endDate=n),{content:[{type:"text",text:JSON.stringify(s,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Search with keyword failed"))}}async handleGetMessages(e){await this.ensureRequiredConnections(!0,!1);const t=e.uids;if(!Array.isArray(t))throw new Error("uids must be an array of numbers");const r=e.markSeen||!1;try{let e=[],n=new Set;if(this.imapClient.getCurrentBox())try{const s=await this.imapClient.fetchMessages(t,{markSeen:r});e.push(...s),s.forEach(e=>n.add(e.uid))}catch(e){console.error(`[GetMessages] Failed to fetch from current mailbox: ${e instanceof Error?e.message:String(e)}`)}const s=t.filter(e=>!n.has(e));if(s.length>0)for(const t of u){if(0===s.length)break;try{await this.imapClient.openBox(t,!0);const o=await this.imapClient.fetchMessages(s,{markSeen:r});o.length>0&&(e.push(...o),o.forEach(e=>{n.add(e.uid);const t=s.indexOf(e.uid);t>-1&&s.splice(t,1)}),o.length)}catch(e){console.error(`[GetMessages] Failed to search in ${t}: ${e instanceof Error?e.message:String(e)}`)}}return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Failed to get messages"))}}async handleGetMessage(e){await this.ensureRequiredConnections(!0,!1);const t=e.uid;if("number"!=typeof t)throw new Error("uid must be a number");const r=e.markSeen||!1;try{const e=await this.findMessageInMultipleMailboxes(t);if(!e)throw new Error(`Message with UID ${t} not found in any mailbox`);if(r)try{const e=await this.imapClient.fetchMessages([t],{markSeen:!0});if(e.length>0)return{content:[{type:"text",text:JSON.stringify(e[0],null,2)}]}}catch(e){console.error(`[GetMessage] Failed to mark message as seen: ${e instanceof Error?e.message:String(e)}`)}return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Failed to get message"))}}async handleDeleteMessage(e){await this.ensureRequiredConnections(!0,!1);const t=e.uid;if("number"!=typeof t)throw new Error("uid must be a number");try{return await this.imapClient.deleteMessage(t),{content:[{type:"text",text:`Message with UID ${t} deleted successfully`}]}}catch(e){throw new Error(this.formatError(e,"Failed to delete message"))}}async handleGetMessageCount(){await this.ensureRequiredConnections(!0,!1);try{return{content:[{type:"text",text:`Total messages: ${await this.imapClient.getMessageCount()}`}]}}catch(e){throw new Error(this.formatError(e,"Failed to get message count"))}}async handleGetUnseenMessages(){await this.ensureRequiredConnections(!0,!1);try{const e=await this.imapClient.getUnseenMessages();return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Failed to get unseen messages"))}}async handleGetRecentMessages(){await this.ensureRequiredConnections(!0,!1);try{const e=await this.imapClient.getRecentMessages();return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Failed to get recent messages"))}}async handleGetConnectionStatus(){const e={timestamp:(new Date).toLocaleString("zh-CN",{timeZone:"Asia/Shanghai",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"}),connections:{imap:{connected:!1,currentBox:null,serverInfo:`${h.IMAP.host}:${h.IMAP.port}`,username:h.IMAP.username,tls:h.IMAP.tls,status:"Not connected"},smtp:{connected:!1,serverInfo:`${h.SMTP.host}:${h.SMTP.port}`,username:h.SMTP.username,secure:h.SMTP.secure,status:"Not connected"}}};return this.imapClient&&(e.connections.imap.connected=this.imapClient.isConnected(),e.connections.imap.currentBox=this.imapClient.getCurrentBox(),e.connections.imap.connected?e.connections.imap.status=e.connections.imap.currentBox?`Connected - Current mailbox: ${e.connections.imap.currentBox}`:"Connected - No mailbox open":e.connections.imap.status="Connection lost or failed"),this.smtpClient&&(e.connections.smtp.connected=!0,e.connections.smtp.status="Connected and ready"),{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}async handleSendEmail(e){await this.ensureRequiredConnections(!1,!0);const t={to:e.to.split(",").map(e=>e.trim()),subject:e.subject,text:e.text,html:e.html,cc:e.cc?e.cc.split(",").map(e=>e.trim()):void 0,bcc:e.bcc?e.bcc.split(",").map(e=>e.trim()):void 0};if(!t.text&&!t.html)throw new Error("Either text or html content is required");try{const e=await this.smtpClient.sendMail(t);await this.saveSentMessage(t,e.messageId);const r={...e,sentFolderSaved:!0,note:"Email sent successfully and saved to sent folder"};return{content:[{type:"text",text:JSON.stringify(r,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Failed to send email"))}}async handleReplyToEmail(e){await this.ensureRequiredConnections(!0,!0);const t=e.originalUid,r=e.text,n=e.html,s=e.replyToAll||!1,o=!1!==e.includeOriginal;if("number"!=typeof t)throw new Error("originalUid must be a number");try{await this.ensureMailboxOpen(),console.error(`[Reply] Fetching original message with UID: ${t}`);const e=await this.findMessageInMultipleMailboxes(t);if(!e)throw new Error(`Original message with UID ${t} not found in any mailbox`);const i=this.extractEmailFromAddress(e.from);if(!i)throw new Error("Could not extract sender email from original message");let a=[i],c=[];if(s){const t=this.extractEmailsFromAddressField(e.to),r=this.extractEmailsFromAddressField(e.cc),n=[...t,...r].filter(e=>e!==h.IMAP.username&&e!==i);n.length>0&&(c=n)}const l=`Re: ${e.subject||""}`;let d=r,m=n;if(o&&e){const t=e.date?new Date(e.date).toLocaleString():"Unknown Date",s=e.from||"Unknown Sender";if(d=`${r}\n\n${this.buildQuotedText(e.text||"",t,s)}`,n||e.html){const o=this.buildQuotedHtml(e.html||e.text||"",t,s);m=`${n||this.textToHtml(r)}<br><br>${o}`}}const u={to:a,cc:c.length>0?c:void 0,subject:l,text:d,html:m};console.error(`[Reply] Sending reply to: ${a.join(", ")}${c.length>0?` (CC: ${c.join(", ")})`:""}`);const p=await this.smtpClient.sendMail(u);await this.saveSentMessage(u,p.messageId);const g={originalUid:t,originalFrom:i,originalSubject:e.subject,replyToAll:s,includeOriginal:o,recipients:{to:a,cc:c.length>0?c:void 0}},f={...p,replyInfo:g,sentFolderSaved:!0,note:"Reply sent successfully and saved to sent folder"};return{content:[{type:"text",text:JSON.stringify(f,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Failed to reply to email"))}}extractEmailFromAddress(e){if(!e)return null;if(Array.isArray(e)&&e.length>0)return e[0].address||e[0];if("string"==typeof e){const t=e.match(/<([^>]+)>/)||e.match(/([^\s<>]+@[^\s<>]+)/);return t?t[1]:e}return e.address||null}extractEmailsFromAddressField(e){if(!e)return[];if(Array.isArray(e))return e.map(e=>e.address||e).filter(Boolean);if("string"==typeof e)return e.split(",").map(e=>{const t=e.trim().match(/<([^>]+)>/)||e.trim().match(/([^\s<>]+@[^\s<>]+)/);return t?t[1]:e.trim()}).filter(Boolean);const t=this.extractEmailFromAddress(e);return t?[t]:[]}cleanReplySubject(e){return e?e.replace(/^(re:|RE:|回复:|答复:)\s*/i,"").trim():""}detectUnrepliedMessagesAdvanced(e,t,r,n){const s=[],o=[...e].sort((e,t)=>new Date(e.date).getTime()-new Date(t.date).getTime()),i=[...t].sort((e,t)=>new Date(e.date).getTime()-new Date(t.date).getTime());console.error(`[IMAP] Analyzing ${o.length} messages from sender and ${i.length} messages to sender`);const a=Math.min(o.length,100),c=o.slice(0,a);a<o.length&&console.error(`[IMAP] Performance optimization: Processing latest ${a} messages out of ${o.length} total`);for(let e=0;e<c.length;e++){const t=c[e];if(this.isMessageReplied(t,i,r))console.error(`[IMAP] Message UID ${t.uid} (${t.subject}) has been replied`);else if(s.push(t),console.error(`[IMAP] Message UID ${t.uid} (${t.subject}) marked as unreplied`),n&&s.length>=n){console.error(`[IMAP] Early termination: Found ${s.length} unreplied messages (limit: ${n}), stopping further analysis for performance`);break}}return s}isMessageReplied(e,t,r){const n=new Date(e.date),s=this.cleanReplySubject(e.subject||"").toLowerCase();if(t.filter(e=>{const t=new Date(e.date),r=this.cleanReplySubject(e.subject||"").toLowerCase();return t>n&&r===s&&r.length>0}).length>0)return console.error(`[IMAP] Found exact subject match reply for "${s}"`),!0;if(s.length>3&&t.filter(e=>{const t=new Date(e.date),r=this.cleanReplySubject(e.subject||"").toLowerCase();return t>n&&r.includes(s)&&r!==s}).length>0)return console.error(`[IMAP] Found thread-based reply for "${s}"`),!0;const o=new Date(n.getTime()+6048e5);return t.filter(t=>{const r=new Date(t.date);return r>n&&r<=o&&this.isLikelyReplyByContent(e,t)}).length>0&&(console.error(`[IMAP] Found time-window based reply for "${s}"`),!0)}isLikelyReplyByContent(e,t){const r=this.cleanReplySubject(e.subject||"").toLowerCase(),n=this.cleanReplySubject(t.subject||"").toLowerCase();if(r===n&&r.length>0)return!0;if(r.length>5&&n.includes(r))return!0;const s=r.split(/\s+/).filter(e=>e.length>3),o=n.split(/\s+/).filter(e=>e.length>3);return s.length>0&&o.length>0&&s.filter(e=>o.includes(e)).length>=Math.min(2,Math.ceil(s.length/2))}buildQuotedText(e,t,r){return`On ${t}, ${r} wrote:\n${e.split("\n").map(e=>`> ${e}`).join("\n")}`}buildQuotedHtml(e,t,r){return`<div style="border-left: 3px solid #ccc; padding-left: 10px; margin-left: 10px; color: #666;">\n <p><strong>On ${t}, ${r} wrote:</strong></p>\n <div>${e.replace(/\n/g,"<br>")}</div>\n </div>`}textToHtml(e){return e.replace(/\n/g,"<br>").replace(/ /g,"&nbsp;&nbsp;")}async findMessageInMultipleMailboxes(e){if(this.imapClient.getCurrentBox())try{return await this.imapClient.getMessage(e)}catch(e){console.error(`[Search] Message not found in current mailbox: ${e instanceof Error?e.message:String(e)}`)}for(const t of u)try{await this.imapClient.openBox(t,!0);const r=await this.imapClient.getMessage(e);return console.error(`[Search] Found message in mailbox: ${t}`),r}catch(e){console.error(`[Search] Message not found in ${t}: ${e instanceof Error?e.message:String(e)}`)}return null}async ensureMailboxOpen(e="INBOX"){this.imapClient.getCurrentBox()||(console.error(`[IMAP] No mailbox currently open, opening ${e}`),await this.imapClient.openBox(e,!0))}async handleConnectAll(){const e=[];try{if(this.imapClient&&this.imapClient.isConnected()){const t=this.imapClient.getCurrentUsername(),r=h.IMAP.username;t!==r?(console.error(`[IMAP] User mismatch: current=${t}, config=${r}, reconnecting...`),await this.imapClient.disconnect(),await this.ensureIMAPConnection(),e.push("✅ IMAP: Reconnected with correct user")):e.push("ℹ️ IMAP: Already connected")}else await this.ensureIMAPConnection(),e.push("✅ IMAP: Connected successfully")}catch(t){e.push(`❌ IMAP: Connection failed - ${t instanceof Error?t.message:String(t)}`)}try{if(this.smtpClient&&this.smtpClient.isConnected()){const t=this.smtpClient.getCurrentUsername(),r=h.SMTP.username;t!==r?(console.error(`[SMTP] User mismatch: current=${t}, config=${r}, reconnecting...`),await this.smtpClient.disconnect(),await this.ensureSMTPConnection(),e.push("✅ SMTP: Reconnected with correct user")):e.push("ℹ️ SMTP: Already connected")}else await this.ensureSMTPConnection(),e.push("✅ SMTP: Connected successfully")}catch(t){e.push(`❌ SMTP: Connection failed - ${t instanceof Error?t.message:String(t)}`)}return{content:[{type:"text",text:e.join("\n")}]}}async handleDisconnectAll(){const e=[];if(this.imapClient)try{await this.imapClient.disconnect(),this.imapClient=null,e.push("✅ IMAP: Disconnected successfully")}catch(t){e.push(`❌ IMAP: Disconnect failed - ${t instanceof Error?t.message:String(t)}`)}else e.push("ℹ️ IMAP: Not connected");if(this.smtpClient)try{await this.smtpClient.disconnect(),this.smtpClient=null,e.push("✅ SMTP: Disconnected successfully")}catch(t){e.push(`❌ SMTP: Disconnect failed - ${t instanceof Error?t.message:String(t)}`)}else e.push("ℹ️ SMTP: Not connected");return{content:[{type:"text",text:e.join("\n")}]}}validateConfig(){try{console.error("=== MCP Mail Server Configuration ==="),console.error(`IMAP: ${h.IMAP.host}:${h.IMAP.port} (TLS: ${h.IMAP.tls})`),console.error(`SMTP: ${h.SMTP.host}:${h.SMTP.port} (Secure: ${h.SMTP.secure})`),console.error(`User: ${h.IMAP.username}`),console.error("Password: [CONFIGURED]"),console.error("Configuration loaded successfully")}catch(e){throw console.error("Configuration error:",e instanceof Error?e.message:String(e)),console.error("Please ensure all required environment variables are set in your MCP server configuration."),e}}buildRawEmailMessage(e,t){const r=(new Date).toUTCString();let n="";if(n+=`Message-ID: ${t||`<${Date.now()}.${Math.random().toString(36)}@${h.IMAP.host}>`}\r\n`,n+=`Date: ${r}\r\n`,n+=`From: ${h.IMAP.username}\r\n`,n+=`To: ${Array.isArray(e.to)?e.to.join(", "):e.to}\r\n`,e.cc&&(n+=`Cc: ${Array.isArray(e.cc)?e.cc.join(", "):e.cc}\r\n`),e.bcc&&(n+=`Bcc: ${Array.isArray(e.bcc)?e.bcc.join(", "):e.bcc}\r\n`),n+=`Subject: ${e.subject}\r\n`,n+="MIME-Version: 1.0\r\n",e.html&&e.text){const t=`----=_Part_${Date.now()}_${Math.random().toString(36)}`;n+=`Content-Type: multipart/alternative; boundary="${t}"\r\n\r\n`,n+=`--${t}\r\n`,n+="Content-Type: text/plain; charset=utf-8\r\n",n+="Content-Transfer-Encoding: 8bit\r\n\r\n",n+=`${e.text}\r\n\r\n`,n+=`--${t}\r\n`,n+="Content-Type: text/html; charset=utf-8\r\n",n+="Content-Transfer-Encoding: 8bit\r\n\r\n",n+=`${e.html}\r\n\r\n`,n+=`--${t}--\r\n`}else e.html?(n+="Content-Type: text/html; charset=utf-8\r\n",n+="Content-Transfer-Encoding: 8bit\r\n\r\n",n+=`${e.html}\r\n`):(n+="Content-Type: text/plain; charset=utf-8\r\n",n+="Content-Transfer-Encoding: 8bit\r\n\r\n",n+=`${e.text||""}\r\n`);return n}async saveSentMessage(e,t){try{await this.ensureRequiredConnections(!0,!1);const r=this.buildRawEmailMessage(e,t);await this.imapClient.saveMessageToFolder(r),console.error("[Email] Message saved to sent folder successfully")}catch(e){console.error("[Email] Failed to save message to sent folder:",e instanceof Error?e.message:String(e))}}async run(){const e=new t;await this.server.connect(e),console.error("MCP Mail server running on stdio")}}).run().catch(console.error);
2
+ import{Server as e}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as t}from"@modelcontextprotocol/sdk/server/stdio.js";import{ListToolsRequestSchema as n,CallToolRequestSchema as r}from"@modelcontextprotocol/sdk/types.js";import s from"imap";import{EventEmitter as a}from"events";import{simpleParser as o}from"mailparser";import i from"nodemailer";import{promises as c}from"node:fs";import l from"node:path";class IMAPClient extends a{imap=null;config;connected=!1;authenticated=!1;currentBox=null;constructor(e){super(),this.config=e}async connect(){return new Promise((e,t)=>{console.error(`[IMAP] Connecting to ${this.config.host}:${this.config.port} (TLS: ${this.config.tls})`);const n={user:this.config.username,password:this.config.password,host:this.config.host,port:this.config.port,tls:this.config.tls||!1,tlsOptions:{rejectUnauthorized:!1,servername:this.config.host},connTimeout:this.config.connTimeout||6e4,authTimeout:this.config.authTimeout||3e4,keepalive:!1!==this.config.keepalive};this.imap=new s(n),this.imap.once("ready",async()=>{console.error("[IMAP] Connection ready"),this.connected=!0,this.authenticated=!0;try{await this.openBox("INBOX",!0),console.error("[IMAP] Auto-opened INBOX")}catch(e){console.error("[IMAP] Failed to auto-open INBOX:",e instanceof Error?e.message:String(e))}e()}),this.imap.once("error",e=>{console.error("[IMAP] Connection error:",e.message),t(new Error(`IMAP connection failed: ${e.message}`))}),this.imap.once("end",()=>{console.error("[IMAP] Connection ended"),this.connected=!1,this.authenticated=!1,this.currentBox=null}),this.imap.connect()})}async openBox(e="INBOX",t=!1){if(!this.imap||!this.authenticated)throw new Error("Not connected or authenticated");return new Promise((n,r)=>{this.imap.openBox(e,t,(t,s)=>{if(t)return console.error(`[IMAP] Failed to open box ${e}:`,t.message),void r(new Error(`Failed to open mailbox: ${t.message}`));console.error(`[IMAP] Opened box ${e}`),this.currentBox=e;const a={name:e,messages:{total:s.messages.total,new:s.messages.new,unseen:s.messages.unseen},permFlags:s.permFlags,uidvalidity:s.uidvalidity,uidnext:s.uidnext};n(a)})})}async getBoxes(){if(!this.imap||!this.authenticated)throw new Error("Not connected or authenticated");return new Promise((e,t)=>{this.imap.getBoxes((n,r)=>{n?t(new Error(`Failed to get boxes: ${n.message}`)):e(r)})})}async search(e=["ALL"]){if(!this.imap)throw new Error("Not connected to IMAP server");return this.currentBox||await this.openBox("INBOX",!0),new Promise((t,n)=>{this.imap.search([e],(e,r)=>{if(e)return console.error("[IMAP] Search failed:",e.message),void n(new Error(`Search failed: ${e.message}`));console.error(`[IMAP] Search found ${r.length} messages`),t(r)})})}async fetchMessages(e,t={}){if(!this.imap)throw new Error("Not connected to IMAP server");this.currentBox||await this.openBox("INBOX",!0);const n=t.includeAttachmentContent||!1,r=Number.isFinite(t.attachmentMaxBytes)?Number(t.attachmentMaxBytes):1048576,s={bodies:t.bodies||["HEADER","TEXT"],struct:!1!==t.struct,envelope:!1!==t.envelope,markSeen:t.markSeen||!1,...t};return new Promise((t,a)=>{const i=[],c=new Map;if(0===e.length)return void t(i);const l=this.imap.fetch(e,s);l.on("message",(e,t)=>{console.error(`[IMAP] Processing message ${t}`);let n={},r="";const s=[],a={uid:0,id:t,flags:[],date:"",size:0};e.on("body",(e,t)=>{const a=[];e.on("data",e=>{a.push(e),s.push(e)}),e.once("end",()=>{const e=Buffer.concat(a);if("HEADER"===t.which){const t=e.toString("utf8");n=this.parseHeaders(t)}else"TEXT"===t.which&&(r=e.toString("utf8"))})}),e.once("attributes",e=>{a.uid=e.uid,a.flags=e.flags||[];const t=e.date||new Date;a.date=t.toLocaleString("zh-CN",{timeZone:"Asia/Shanghai",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"}),a.size=e.size||0}),e.once("end",()=>{console.error(`[IMAP] Message ${t} processed, preparing for parse`),c.set(t,{message:a,headers:n,body:r,rawBuffer:Buffer.concat(s)})})}),l.once("error",e=>{console.error("[IMAP] Fetch error:",e.message),a(new Error(`Fetch failed: ${e.message}`))}),l.once("end",async()=>{console.error(`[IMAP] Fetch completed, parsing ${c.size} messages`);for(const[e,t]of c)try{const e=await o(t.rawBuffer),s=e=>e?Array.isArray(e)?e.map(e=>a(e)).filter(Boolean).join(", "):a(e):"",a=e=>{if(!e)return"";if("string"==typeof e){const t=e.match(/<([^>]+)>/)||e.match(/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/);return t?t[1]:e}if(e&&"object"==typeof e){if(e.address)return e.address;if(e.text){const t=e.text.match(/<([^>]+)>/)||e.text.match(/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/);return t?t[1]:e.text}}return""};i.push({...t.message,subject:e.subject||"No Subject",from:s(e.from),to:s(e.to),cc:s(e.cc)||void 0,bcc:s(e.bcc)||void 0,text:e.text,html:e.html,attachments:(e.attachments||[]).map(e=>{const t=e.content,s=n&&Buffer.isBuffer(t)&&t.length<=r;return{filename:e.filename||"unnamed-attachment",contentType:e.contentType,size:e.size||(Buffer.isBuffer(t)?t.length:0),checksum:e.checksum,contentId:e.cid,contentDisposition:e.contentDisposition,transferEncoding:e.transferEncoding,contentBase64:s?t.toString("base64"):void 0,contentTruncated:!(!n||!Buffer.isBuffer(t))&&t.length>r}})})}catch(n){console.error(`[IMAP] Failed to parse message ${e}:`,n),i.push({...t.message,subject:t.headers.subject||"Parse Failed",from:t.headers.from||"",to:t.headers.to||"",cc:t.headers.cc||void 0,bcc:t.headers.bcc||void 0,text:t.body.trim()})}console.error(`[IMAP] All messages parsed, returning ${i.length} messages`),t(i)})})}async getMessage(e,t={}){const n=await this.fetchMessages([e],t);if(0===n.length)throw new Error(`Message with UID ${e} not found`);return n[0]}async deleteMessage(e){if(!this.imap)throw new Error("Not connected to IMAP server");return this.currentBox||await this.openBox("INBOX",!1),new Promise((t,n)=>{this.imap.addFlags(e,["\\Deleted"],r=>{if(r)return console.error(`[IMAP] Failed to mark message ${e} as deleted:`,r.message),void n(new Error(`Failed to delete message: ${r.message}`));console.error(`[IMAP] Message ${e} marked for deletion`),this.imap.expunge(r=>{if(r)return console.error("[IMAP] Failed to expunge:",r.message),void n(new Error(`Failed to expunge deleted messages: ${r.message}`));console.error(`[IMAP] Message ${e} deleted successfully`),t()})})})}async getMessageCount(){return this.currentBox||await this.openBox("INBOX",!0),(await this.search(["ALL"])).length}async getUnseenMessages(){const e=await this.search(["UNSEEN"]);return this.fetchMessages(e)}async getRecentMessages(){const e=await this.search(["RECENT"]);return this.fetchMessages(e)}parseHeaders(e){const t={},n=e.split("\r\n");let r="",s="";for(const e of n)if(e.match(/^\s/)&&r)s+=" "+e.trim();else{r&&(t[r.toLowerCase()]=s.trim());const n=e.indexOf(":");n>-1?(r=e.substring(0,n).trim(),s=e.substring(n+1).trim()):(r="",s="")}return r&&(t[r.toLowerCase()]=s.trim()),t}async disconnect(){if(this.imap)return this.connected?new Promise(e=>{const t=setTimeout(()=>{console.error("[IMAP] Disconnect timeout, forcing cleanup"),this.connected=!1,this.authenticated=!1,this.currentBox=null,this.imap=null,e()},5e3);this.imap.once("end",()=>{clearTimeout(t),console.error("[IMAP] Disconnected"),this.connected=!1,this.authenticated=!1,this.currentBox=null,this.imap=null,e()}),this.imap.once("error",n=>{clearTimeout(t),console.error("[IMAP] Disconnect error:",n.message),this.connected=!1,this.authenticated=!1,this.currentBox=null,this.imap=null,e()});try{this.imap.end()}catch(n){clearTimeout(t),console.error("[IMAP] Error calling end():",n),this.connected=!1,this.authenticated=!1,this.currentBox=null,this.imap=null,e()}}):(this.imap=null,this.authenticated=!1,void(this.currentBox=null))}isConnected(){return this.connected&&this.authenticated}getCurrentBox(){return this.currentBox}getCurrentUsername(){return this.config?.username||null}async saveMessageToFolder(e,t="INBOX.Sent"){if(!this.connected)throw new Error("IMAP client is not connected");return new Promise((n,r)=>{this.imap.openBox(t,!1,s=>{s?(console.warn(`[IMAP] Folder ${t} not found, trying to create it`),this.imap.addBox(t,s=>{if(s)return console.error(`[IMAP] Failed to create folder ${t}:`,s.message),void this.trySaveToAlternateSentFolders(e,n,r);this.saveToOpenedFolder(e,t,n,r)})):this.saveToOpenedFolder(e,t,n,r)})})}saveToOpenedFolder(e,t,n,r){this.imap.append(e,{mailbox:t},e=>{e?(console.error(`[IMAP] Failed to save message to ${t}:`,e.message),r(new Error(`Failed to save message to ${t}: ${e.message}`))):n()})}trySaveToAlternateSentFolders(e,t,n){const r=["Sent","SENT","Sent Items","Sent Messages","已发送"];let s=0;const a=()=>{if(s>=r.length)return console.warn("[IMAP] All sent folder attempts failed, message not saved to sent folder"),void t();const o=r[s++];this.imap.openBox(o,!1,r=>{r?a():this.saveToOpenedFolder(e,o,t,n)})};a()}}class SMTPClient{transporter=null;config;constructor(e){this.config=e}async connect(){this.transporter=i.createTransport({host:this.config.host,port:this.config.port,secure:this.config.secure||!1,auth:{user:this.config.username,pass:this.config.password}});try{this.transporter&&await this.transporter.verify()}catch(e){throw new Error(`SMTP connection failed: ${e instanceof Error?e.message:String(e)}`)}}async sendMail(e){if(!this.transporter)throw new Error("SMTP client not connected");const t={from:e.from||this.config.username,to:Array.isArray(e.to)?e.to.join(", "):e.to,cc:e.cc?Array.isArray(e.cc)?e.cc.join(", "):e.cc:void 0,bcc:e.bcc?Array.isArray(e.bcc)?e.bcc.join(", "):e.bcc:void 0,subject:e.subject,text:e.text,html:e.html,attachments:e.attachments};try{const e=await this.transporter.sendMail(t);return{messageId:e.messageId,response:e.response,accepted:e.accepted||[],rejected:e.rejected||[]}}catch(e){throw new Error(`Failed to send email: ${e instanceof Error?e.message:String(e)}`)}}getCurrentUsername(){return this.config?.username||null}isConnected(){return null!==this.transporter}async disconnect(){if(this.transporter)try{this.transporter.close(),console.error("[SMTP] Disconnected successfully")}catch(e){console.error("[SMTP] Error during disconnect:",e instanceof Error?e.message:String(e))}finally{this.transporter=null}}}function d(e,t){const n=process.env[e];if(!n)throw new Error(`Missing required environment variable: ${e}. Please set this variable in your MCP server configuration.`);return n}function m(e){const t=process.env[e];if(!t)throw new Error(`Missing required environment variable: ${e}. Please set this variable to 'true' or 'false' in your MCP server configuration.`);if("true"!==t.toLowerCase()&&"false"!==t.toLowerCase())throw new Error(`Invalid boolean value for environment variable ${e}: ${t}. Must be 'true' or 'false'.`);return"true"===t.toLowerCase()}function h(e){const t=process.env[e];if(!t)throw new Error(`Missing required environment variable: ${e}. Please set this variable to a valid number in your MCP server configuration.`);const n=parseInt(t,10);if(isNaN(n))throw new Error(`Invalid number value for environment variable ${e}: ${t}. Must be a valid number.`);return n}const u={IMAP:{host:d("IMAP_HOST"),port:h("IMAP_PORT"),username:d("EMAIL_USER"),password:d("EMAIL_PASS"),tls:m("IMAP_SECURE")},SMTP:{host:d("SMTP_HOST"),port:h("SMTP_PORT"),username:d("EMAIL_USER"),password:d("EMAIL_PASS"),secure:m("SMTP_SECURE")}},p=["INBOX.Sent","Sent","SENT","Sent Items","Sent Messages","已发送"],f=["INBOX",...p];class MailMCPServer{server;imapClient=null;smtpClient=null;isInitializing=!1;formatError(e,t){return`${t}: ${e instanceof Error?e.message:String(e)}`}parseAttachments(e){if(e){if(!Array.isArray(e))throw new Error("attachments must be an array");return e.map((e,t)=>{if(!e||"object"!=typeof e)throw new Error(`attachments[${t}] must be an object`);if(!e.filename||"string"!=typeof e.filename)throw new Error(`attachments[${t}].filename must be a non-empty string`);if("string"!=typeof e.content)throw new Error(`attachments[${t}].content must be a string`);const n=e.encoding||"utf8";if("utf8"!==n&&"base64"!==n)throw new Error(`attachments[${t}].encoding must be either "utf8" or "base64"`);return{filename:e.filename,content:e.content,contentType:e.contentType,encoding:n}})}}normalizeAttachmentBase64(e,t){return"base64"===t?e.replace(/\s+/g,""):Buffer.from(e,"utf8").toString("base64")}isDateOnly(e){return[/^\d{4}-\d{2}-\d{2}$/,/^\d{2}-\w{3}-\d{4}$/,/^\w{3}\s+\d{1,2},?\s+\d{4}$/].some(t=>t.test(e.trim()))}filterMessagesByDateRange(e,t,n){if(!t&&!n)return e;let r=null,s=null;t&&(r=new Date(t),isNaN(r.getTime())?(console.error(`[Filter] Invalid start date format: ${t}, skipping start date filter`),r=null):console.error(`[Filter] Start date parsed as: ${r.toISOString()}`)),n&&(s=new Date(n),isNaN(s.getTime())?(console.error(`[Filter] Invalid end date format: ${n}, skipping end date filter`),s=null):this.isDateOnly(n)?(s.setHours(23,59,59,999),console.error(`[Filter] End date adjusted to end of day: ${s.toISOString()}`)):console.error(`[Filter] End date parsed as: ${s.toISOString()}`));const a=e.length,o=e.filter(e=>{if(!e.date)return!0;const t=new Date(e.date);return!(!isNaN(t.getTime())&&(r&&t<r||s&&t>s))});return console.error(`[Filter] Date filtering: ${a} -> ${o.length} messages`),o}async searchInMultipleMailboxes(e,t,n,r="",s="",a){const o=["INBOX"];let i=!1;for(const e of p)try{await this.imapClient.openBox(e,!0),o.push(e),i=!0;break}catch(t){console.error(`[IMAP] Failed to open sent mailbox ${e}: ${t instanceof Error?t.message:String(t)}`)}const c={searchType:t,searchValue:n,searchCriteria:e,mailboxesSearched:[],totalMatches:0,messages:[]};for(const t of o)try{console.error(`[IMAP] Searching in mailbox: ${t}`),await this.imapClient.openBox(t,!0);const n=await this.imapClient.search(e);console.error(`[IMAP] Found ${n.length} messages in ${t}`);let o=[],i=[];if(n.length>0){const e=a?n.slice(0,a):n;console.error(`[IMAP] Auto-fetching content for ${e.length} messages from ${t}${a?` (limited from ${n.length})`:""}`);let l=(await this.imapClient.fetchMessages(e)).map(e=>({...e,sourceMailbox:t}));(r||s)&&(l=this.filterMessagesByDateRange(l,r,s)),o=l,i=l.map(e=>e.uid),c.messages.push(...o)}const l={mailbox:t,matchingUIDs:i,messageCount:o.length};c.mailboxesSearched.push(l)}catch(e){console.error(`[IMAP] Error searching in ${t}:`,e),c.mailboxesSearched.push({mailbox:t,error:`Failed to search: ${e instanceof Error?e.message:String(e)}`,matchingUIDs:[],messageCount:0})}if(c.totalMatches=c.messages.length,c.messages.sort((e,t)=>{const n=new Date(e.date||0);return new Date(t.date||0).getTime()-n.getTime()}),c.totalMatches>0){let e=`Found and retrieved ${c.totalMatches} messages across ${c.mailboxesSearched.length} mailboxes`;(r||s)&&(e+=" (filtered by date range)"),c.note=e}else c.note="No messages found in any of the searched mailboxes";return i||(c.warning="Could not find sent mailbox - only searched INBOX"),c}constructor(){this.validateConfig(),this.server=new e({name:"mcp-mail",version:"1.0.0"},{capabilities:{tools:{}}}),this.setupToolHandlers(),this.setupErrorHandling()}setupErrorHandling(){this.server.onerror=e=>console.error("[MCP Error]",e),process.on("SIGINT",async()=>{this.imapClient&&await this.imapClient.disconnect(),this.smtpClient&&await this.smtpClient.disconnect(),await this.server.close(),process.exit(0)})}setupToolHandlers(){this.server.setRequestHandler(n,async()=>({tools:[{name:"connect_all",description:"Connect to both IMAP and SMTP servers simultaneously",inputSchema:{type:"object",properties:{}}},{name:"list_mailboxes",description:"List all available mailboxes (folders). Auto-connects if not already connected.",inputSchema:{type:"object",properties:{}}},{name:"open_mailbox",description:"Open a specific mailbox (folder) and optionally retrieve sent mailbox info. Due to IMAP protocol limitations, only one mailbox stays open. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{mailboxName:{type:"string",description:"Name of the mailbox to open (default: INBOX)",default:"INBOX"},readOnly:{type:"boolean",description:"Open mailbox in read-only mode (default: false)",default:!1},openSent:{type:"boolean",description:"Also retrieve sent mailbox information (default: true)",default:!0}}}},{name:"get_message_count",description:"Get the total number of messages in current mailbox. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{}}},{name:"get_unseen_messages",description:"Get all unseen (unread) messages. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{}}},{name:"get_recent_messages",description:"Get all recent messages. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{}}},{name:"search_by_sender",description:"Search messages from a specific sender with optional date range. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{sender:{type:"string",description:"Email address of the sender to search for"},startDate:{type:"string",description:'Optional start date/time for filtering. Supports multiple formats: "2025-07-01", "2025-07-01 14:30:00", "01-Jul-2025", or ISO format. Leave empty to not filter by start date.'},endDate:{type:"string",description:'Optional end date/time for filtering. Supports multiple formats: "2025-08-01", "2025-08-01 23:59:59", "01-Aug-2025", or ISO format. Leave empty to not filter by end date.'}},required:["sender"]}},{name:"search_by_subject",description:"Search messages by subject keywords with optional date range. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{subject:{type:"string",description:"Keywords to search in email subject"},startDate:{type:"string",description:'Optional start date/time for filtering. Supports multiple formats: "2025-07-01", "2025-07-01 14:30:00", "01-Jul-2025", or ISO format. Leave empty to not filter by start date.'},endDate:{type:"string",description:'Optional end date/time for filtering. Supports multiple formats: "2025-08-01", "2025-08-01 23:59:59", "01-Aug-2025", or ISO format. Leave empty to not filter by end date.'}},required:["subject"]}},{name:"search_by_recipient",description:"Search messages sent to a specific recipient email address with optional date range. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{recipient:{type:"string",description:"Email address of the recipient to search for"},startDate:{type:"string",description:'Optional start date/time for filtering. Supports multiple formats: "2025-07-01", "2025-07-01 14:30:00", "01-Jul-2025", or ISO format. Leave empty to not filter by start date.'},endDate:{type:"string",description:'Optional end date/time for filtering. Supports multiple formats: "2025-08-01", "2025-08-01 23:59:59", "01-Aug-2025", or ISO format. Leave empty to not filter by end date.'}},required:["recipient"]}},{name:"search_since_date",description:"Search messages from a specific date until now (not for date ranges). Use search_messages for complex date ranges.",inputSchema:{type:"object",properties:{date:{type:"string",description:'Start date to search from (searches from this date to present). Formats: "April 20, 2010", "20-Apr-2010", or "2010-04-20"'}},required:["date"]}},{name:"search_unread_from_sender",description:"Search unread messages from a specific sender with optional date range (demonstrates AND logic). Auto-connects if not already connected.",inputSchema:{type:"object",properties:{sender:{type:"string",description:"Email address of the sender"},startDate:{type:"string",description:'Optional start date/time for filtering. Supports multiple formats: "2025-07-01", "2025-07-01 14:30:00", "01-Jul-2025", or ISO format. Leave empty to not filter by start date.'},endDate:{type:"string",description:'Optional end date/time for filtering. Supports multiple formats: "2025-08-01", "2025-08-01 23:59:59", "01-Aug-2025", or ISO format. Leave empty to not filter by end date.'}},required:["sender"]}},{name:"search_unreplied_from_sender",description:"Search unreplied messages from a specific sender with optional date range. Identifies messages that have not been replied to by checking for corresponding reply messages. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{sender:{type:"string",description:"Email address of the sender to search for unreplied messages"},startDate:{type:"string",description:'Optional start date/time for filtering. Supports multiple formats: "2025-07-01", "2025-07-01 14:30:00", "01-Jul-2025", or ISO format. Leave empty to not filter by start date.'},endDate:{type:"string",description:'Optional end date/time for filtering. Supports multiple formats: "2025-08-01", "2025-08-01 23:59:59", "01-Aug-2025", or ISO format. Leave empty to not filter by end date.'},limit:{type:"number",description:"Maximum number of messages to process from each search (default: 10, maximum: 200). Since unreplied emails are typically few, smaller limits are recommended.",default:10}},required:["sender"]}},{name:"search_by_body",description:"Search messages containing specific text in the body with optional date range. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{text:{type:"string",description:"Text to search for in message body"},startDate:{type:"string",description:'Optional start date/time for filtering. Supports multiple formats: "2025-07-01", "2025-07-01 14:30:00", "01-Jul-2025", or ISO format. Leave empty to not filter by start date.'},endDate:{type:"string",description:'Optional end date/time for filtering. Supports multiple formats: "2025-08-01", "2025-08-01 23:59:59", "01-Aug-2025", or ISO format. Leave empty to not filter by end date.'}},required:["text"]}},{name:"search_with_keyword",description:"Search messages with specific keyword/flag with optional date range. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{keyword:{type:"string",description:"Keyword to search for"},startDate:{type:"string",description:'Optional start date/time for filtering. Supports multiple formats: "2025-07-01", "2025-07-01 14:30:00", "01-Jul-2025", or ISO format. Leave empty to not filter by start date.'},endDate:{type:"string",description:'Optional end date/time for filtering. Supports multiple formats: "2025-08-01", "2025-08-01 23:59:59", "01-Aug-2025", or ISO format. Leave empty to not filter by end date.'}},required:["keyword"]}},{name:"get_messages",description:"Retrieve multiple messages by their UIDs. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{uids:{type:"array",description:"Array of message UIDs to retrieve",items:{type:"number"}},markSeen:{type:"boolean",description:"Mark messages as seen when retrieving (default: false)",default:!1},includeAttachmentContent:{type:"boolean",description:"Include attachment content as base64 in each message (default: false)",default:!1},attachmentMaxBytes:{type:"number",description:"Max attachment bytes to inline when includeAttachmentContent=true (default: 1048576)",default:1048576}},required:["uids"]}},{name:"get_message",description:"Retrieve a specific email message by UID. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{uid:{type:"number",description:"Message UID to retrieve"},markSeen:{type:"boolean",description:"Mark message as seen when retrieving (default: false)",default:!1},includeAttachmentContent:{type:"boolean",description:"Include attachment content as base64 in response (default: true)",default:!0},attachmentMaxBytes:{type:"number",description:"Max attachment bytes to inline when includeAttachmentContent=true (default: 1048576)",default:1048576}},required:["uid"]}},{name:"export_attachment",description:"Export one attachment from a message to local file path.",inputSchema:{type:"object",properties:{uid:{type:"number",description:"Message UID to export attachment from"},filePath:{type:"string",description:"Target local file path for exported attachment"},attachmentIndex:{type:"number",description:"Attachment index in attachments array (default: 0)",default:0},filename:{type:"string",description:"Optional attachment filename to select by name (takes precedence over attachmentIndex)"}},required:["uid","filePath"]}},{name:"send_email",description:"Send an email via SMTP. Auto-connects to SMTP server if not already connected.",inputSchema:{type:"object",properties:{to:{type:"string",description:"Recipient email address(es), comma-separated"},subject:{type:"string",description:"Email subject"},text:{type:"string",description:"Plain text email body"},html:{type:"string",description:"HTML email body (optional)"},cc:{type:"string",description:"CC recipients, comma-separated (optional)"},bcc:{type:"string",description:"BCC recipients, comma-separated (optional)"},attachments:{type:"array",description:'Optional attachments. Use content as UTF-8 text by default, or base64 when encoding is "base64".',items:{type:"object",properties:{filename:{type:"string",description:"Attachment filename"},content:{type:"string",description:'Attachment content (UTF-8 text by default, or base64 string when encoding is "base64")'},contentType:{type:"string",description:"MIME content type (optional)"},encoding:{type:"string",description:'Content encoding, either "utf8" or "base64" (default: "utf8")',enum:["utf8","base64"]}},required:["filename","content"]}}},required:["to","subject"]}},{name:"reply_to_email",description:"Reply to a specific email by UID. Automatically sets reply headers, adds Re: prefix, and includes original message. Auto-connects to both IMAP and SMTP if not already connected.",inputSchema:{type:"object",properties:{originalUid:{type:"number",description:"UID of the original message to reply to"},text:{type:"string",description:"Reply message text"},html:{type:"string",description:"Reply message HTML (optional)"},replyToAll:{type:"boolean",description:"Reply to all recipients instead of just sender (default: false)",default:!1},includeOriginal:{type:"boolean",description:"Include original message in reply (default: true)",default:!0}},required:["originalUid"]}},{name:"delete_message",description:"Delete a specific email message by UID. Auto-connects if not already connected.",inputSchema:{type:"object",properties:{uid:{type:"number",description:"Message UID to delete"}},required:["uid"]}},{name:"get_connection_status",description:"Check the current connection status of both IMAP and SMTP servers.",inputSchema:{type:"object",properties:{}}},{name:"disconnect_all",description:"Disconnect from both IMAP and SMTP servers. Only disconnects if currently connected.",inputSchema:{type:"object",properties:{}}}]})),this.server.setRequestHandler(r,async e=>{const{name:t,arguments:n}=e.params;try{switch(t){case"open_mailbox":return await this.handleOpenMailbox(n||{});case"list_mailboxes":return await this.handleListMailboxes();case"search_by_sender":return await this.handleSearchBySender(n||{});case"search_by_subject":return await this.handleSearchBySubject(n);case"search_by_recipient":return await this.handleSearchByRecipient(n);case"search_since_date":return await this.handleSearchSinceDate(n);case"search_unread_from_sender":return await this.handleSearchUnreadFromSender(n);case"search_unreplied_from_sender":return await this.handleSearchUnrepliedFromSender(n);case"search_by_body":return await this.handleSearchByBody(n);case"search_with_keyword":return await this.handleSearchWithKeyword(n);case"get_messages":return await this.handleGetMessages(n);case"get_message":return await this.handleGetMessage(n);case"export_attachment":return await this.handleExportAttachment(n);case"delete_message":return await this.handleDeleteMessage(n);case"get_message_count":return await this.handleGetMessageCount();case"get_unseen_messages":return await this.handleGetUnseenMessages();case"get_recent_messages":return await this.handleGetRecentMessages();case"get_connection_status":return await this.handleGetConnectionStatus();case"send_email":return await this.handleSendEmail(n);case"reply_to_email":return await this.handleReplyToEmail(n);case"connect_all":return await this.handleConnectAll();case"disconnect_all":return await this.handleDisconnectAll();default:throw new Error(`Unknown tool: ${t}`)}}catch(e){return{content:[{type:"text",text:`Error: ${e instanceof Error?e.message:String(e)}`}]}}})}async ensureIMAPConnection(){if(!this.imapClient||!this.imapClient.isConnected())if(this.isInitializing)for(;this.isInitializing;)await new Promise(e=>setTimeout(e,100));else{this.isInitializing=!0;try{const e=u.IMAP;console.error(`[IMAP] Auto-connecting to ${e.host}:${e.port}`),this.imapClient=new IMAPClient(e),await this.imapClient.connect(),console.error("[IMAP] Auto-connection successful")}finally{this.isInitializing=!1}}}async ensureSMTPConnection(){if(this.smtpClient)return;const e=u.SMTP;console.error(`[SMTP] Auto-connecting to ${e.host}:${e.port}`),this.smtpClient=new SMTPClient(e),await this.smtpClient.connect(),console.error("[SMTP] Auto-connection successful")}async ensureRequiredConnections(e=!1,t=!1){e&&await this.ensureIMAPConnection(),t&&await this.ensureSMTPConnection()}async handleOpenMailbox(e){await this.ensureRequiredConnections(!0,!1);const t=e.mailboxName||"INBOX",n=e.readOnly||!1,r=!1!==e.openSent;try{const e={},s=await this.imapClient.openBox(t,n);if(e[t]=s,e.currentlyOpen=t,r&&"INBOX.Sent"!==t&&"Sent"!==t&&"SENT"!==t)try{const r=["INBOX.Sent","Sent","SENT","Sent Items","Sent Messages","已发送"];let s=!1;for(const a of r)try{const r=await this.imapClient.openBox(a,!0);e[a]=r,s=!0,await this.imapClient.openBox(t,n),e.currentlyOpen=t,e.note=`Retrieved info from both ${t} and ${a}. Currently open: ${t}`;break}catch(e){console.error(`[IMAP] Failed to open sent mailbox ${a}: ${e instanceof Error?e.message:String(e)}`)}s||(e.sentBoxWarning="Could not find any sent mailbox")}catch(t){e.sentBoxError=`Failed to access sent mailbox: ${t instanceof Error?t.message:String(t)}`}return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Failed to open mailbox"))}}async handleListMailboxes(){await this.ensureRequiredConnections(!0,!1);try{const e=await this.imapClient.getBoxes(),t=(e,n="",r=new Set)=>{if(r.has(e))return{name:n,circular:!0};r.add(e);const s={name:n,attribs:e.attribs||[],delimiter:e.delimiter||".",selectable:!e.attribs?.includes("\\Noselect")};if(e.children&&Object.keys(e.children).length>0){s.children={};for(const[a,o]of Object.entries(e.children)){const i=n?`${n}${e.delimiter||"."}${a}`:a;s.children[a]=t(o,i,new Set(r))}}return r.delete(e),s},n={};for(const[r,s]of Object.entries(e))n[r]=t(s,r);return{content:[{type:"text",text:JSON.stringify(n,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Failed to list mailboxes"))}}async handleSearchBySender(e){await this.ensureRequiredConnections(!0,!1);const t=e.sender,n=e.startDate||"",r=e.endDate||"";if(!t)throw new Error("sender parameter is required");try{const e=["FROM",t];console.error("[IMAP] Searching messages from sender across all mailboxes:",t);const s=await this.searchInMultipleMailboxes(e,"By Sender",t,n,r);return s.sender=t,n&&(s.startDate=n),r&&(s.endDate=r),{content:[{type:"text",text:JSON.stringify(s,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Search by sender failed"))}}async handleSearchBySubject(e){await this.ensureRequiredConnections(!0,!1);const t=e.subject,n=e.startDate||"",r=e.endDate||"";if(!t)throw new Error("subject parameter is required");try{const e=["SUBJECT",t];console.error("[IMAP] Searching messages with subject across all mailboxes:",t);const s=await this.searchInMultipleMailboxes(e,"By Subject",t,n,r);return s.subjectKeywords=t,n&&(s.startDate=n),r&&(s.endDate=r),{content:[{type:"text",text:JSON.stringify(s,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Search by subject failed"))}}async handleSearchByRecipient(e){await this.ensureRequiredConnections(!0,!1);const t=e.recipient,n=e.startDate||"",r=e.endDate||"";if(!t)throw new Error("recipient parameter is required");try{const e=["TO",t];console.error("[IMAP] Searching messages to recipient across all mailboxes:",t);const s=await this.searchInMultipleMailboxes(e,"By Recipient",t,n,r);return s.recipient=t,n&&(s.startDate=n),r&&(s.endDate=r),{content:[{type:"text",text:JSON.stringify(s,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Search by recipient failed"))}}async handleSearchSinceDate(e){await this.ensureRequiredConnections(!0,!1);const t=e.date;if(!t)throw new Error("date parameter is required");try{const e=["SINCE",t];console.error("[IMAP] Searching messages since date across all mailboxes:",t);const n=await this.searchInMultipleMailboxes(e,"Since Date",t);return n.sinceDate=t,n.note='Date format should be like "April 20, 2010" or "20-Apr-2010". Searched across multiple mailboxes.',{content:[{type:"text",text:JSON.stringify(n,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Search since date failed"))}}async handleSearchUnreadFromSender(e){await this.ensureRequiredConnections(!0,!1);const t=e.sender,n=e.startDate||"",r=e.endDate||"";if(!t)throw new Error("sender parameter is required");try{const e=["UNSEEN",["FROM",t]];console.error("[IMAP] Searching unread messages from sender across all mailboxes:",t);const s=await this.searchInMultipleMailboxes(e,"Unread messages from specific sender",t,n,r);return s.sender=t,n&&(s.startDate=n),r&&(s.endDate=r),s.note="By default, all criteria are ANDed together - finds messages that are BOTH unread AND from the specified sender. Searched across multiple mailboxes.",{content:[{type:"text",text:JSON.stringify(s,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Search unread from sender failed"))}}async handleSearchUnrepliedFromSender(e){await this.ensureRequiredConnections(!0,!1);const t=e.sender,n=e.startDate||"",r=e.endDate||"",s=Math.min(Math.max(e.limit||10,1),200);if(!t)throw new Error("sender parameter is required");try{console.error(`[IMAP] Searching unreplied messages from sender: ${t}`);const e=await this.searchInMultipleMailboxes(["FROM",t],"From Sender",t,n,r,s),a=await this.searchInMultipleMailboxes(["TO",t],"To Sender",t,n,r,s);console.error(`[IMAP] Found ${e.messages.length} messages from sender, ${a.messages.length} messages to sender`),e.messages.length>s&&(console.error(`[IMAP] Warning: Found ${e.messages.length} messages from sender, processing only the latest ${s}`),e.messages=e.messages.sort((e,t)=>new Date(t.date).getTime()-new Date(e.date).getTime()).slice(0,s));const o=this.detectUnrepliedMessagesAdvanced(e.messages,a.messages,t,s);console.error(`[IMAP] Found ${o.length} unreplied messages using advanced detection`);const i={searchType:"Unreplied messages from sender (Advanced)",searchValue:t,searchCriteria:["FROM",t],mailboxesSearched:e.mailboxesSearched,totalMatches:o.length,messages:o,sender:t,note:`Found ${o.length} unreplied messages from ${t} using advanced thread-aware detection.${s<200?` (limited to ${s} messages per search)`:""}`};return n&&(i.startDate=n),r&&(i.endDate=r),(n||r)&&(i.note+=" (filtered by date range)"),{content:[{type:"text",text:JSON.stringify(i,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Search unreplied from sender failed"))}}async handleSearchByBody(e){await this.ensureRequiredConnections(!0,!1);const t=e.text,n=e.startDate||"",r=e.endDate||"";if(!t)throw new Error("text parameter is required");try{const e=["BODY",t];console.error("[IMAP] Searching messages with body text across all mailboxes:",t);const s=await this.searchInMultipleMailboxes(e,"By Body Text",t,n,r);return s.bodyText=t,n&&(s.startDate=n),r&&(s.endDate=r),{content:[{type:"text",text:JSON.stringify(s,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Search by body failed"))}}async handleSearchWithKeyword(e){await this.ensureRequiredConnections(!0,!1);const t=e.keyword,n=e.startDate||"",r=e.endDate||"";if(!t)throw new Error("keyword parameter is required");try{const e=["KEYWORD",t];console.error("[IMAP] Searching messages with keyword across all mailboxes:",t);const s=await this.searchInMultipleMailboxes(e,"With Keyword",t,n,r);return s.keyword=t,n&&(s.startDate=n),r&&(s.endDate=r),{content:[{type:"text",text:JSON.stringify(s,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Search with keyword failed"))}}async handleGetMessages(e){await this.ensureRequiredConnections(!0,!1);const t=e.uids;if(!Array.isArray(t))throw new Error("uids must be an array of numbers");const n=e.markSeen||!1,r=e.includeAttachmentContent||!1,s="number"==typeof e.attachmentMaxBytes?e.attachmentMaxBytes:1048576;try{let e=[],a=new Set;if(this.imapClient.getCurrentBox())try{const o=await this.imapClient.fetchMessages(t,{markSeen:n,includeAttachmentContent:r,attachmentMaxBytes:s});e.push(...o),o.forEach(e=>a.add(e.uid))}catch(e){console.error(`[GetMessages] Failed to fetch from current mailbox: ${e instanceof Error?e.message:String(e)}`)}const o=t.filter(e=>!a.has(e));if(o.length>0)for(const t of f){if(0===o.length)break;try{await this.imapClient.openBox(t,!0);const i=await this.imapClient.fetchMessages(o,{markSeen:n,includeAttachmentContent:r,attachmentMaxBytes:s});i.length>0&&(e.push(...i),i.forEach(e=>{a.add(e.uid);const t=o.indexOf(e.uid);t>-1&&o.splice(t,1)}),i.length)}catch(e){console.error(`[GetMessages] Failed to search in ${t}: ${e instanceof Error?e.message:String(e)}`)}}return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Failed to get messages"))}}async handleGetMessage(e){await this.ensureRequiredConnections(!0,!1);const t=e.uid;if("number"!=typeof t)throw new Error("uid must be a number");const n=e.markSeen||!1,r=!1!==e.includeAttachmentContent,s="number"==typeof e.attachmentMaxBytes?e.attachmentMaxBytes:1048576;try{const e=await this.findMessageInMultipleMailboxes(t,{includeAttachmentContent:r,attachmentMaxBytes:s});if(!e)throw new Error(`Message with UID ${t} not found in any mailbox`);if(n)try{const e=await this.imapClient.fetchMessages([t],{markSeen:!0,includeAttachmentContent:r,attachmentMaxBytes:s});if(e.length>0)return{content:[{type:"text",text:JSON.stringify(e[0],null,2)}]}}catch(e){console.error(`[GetMessage] Failed to mark message as seen: ${e instanceof Error?e.message:String(e)}`)}return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Failed to get message"))}}async handleExportAttachment(e){await this.ensureRequiredConnections(!0,!1);const t=e.uid;if("number"!=typeof t)throw new Error("uid must be a number");if(!e.filePath||"string"!=typeof e.filePath)throw new Error("filePath is required and must be a string");const n="number"==typeof e.attachmentIndex?e.attachmentIndex:0;if(n<0)throw new Error("attachmentIndex must be >= 0");const r=await this.findMessageInMultipleMailboxes(t,{includeAttachmentContent:!0,attachmentMaxBytes:Number.MAX_SAFE_INTEGER});if(!r)throw new Error(`Message with UID ${t} not found in any mailbox`);const s=r.attachments||[];if(0===s.length)throw new Error(`Message ${t} has no attachments`);const a=e.filename?s.find(t=>t.filename===e.filename):s[n];if(!a){if(e.filename)throw new Error(`Attachment "${e.filename}" not found in message ${t}`);throw new Error(`Attachment index ${n} out of range, total attachments: ${s.length}`)}if(!a.contentBase64)throw new Error(`Attachment "${a.filename}" content is unavailable`);const o=l.resolve(process.cwd(),e.filePath);await c.mkdir(l.dirname(o),{recursive:!0});const i=Buffer.from(a.contentBase64,"base64");return await c.writeFile(o,i),{content:[{type:"text",text:JSON.stringify({success:!0,uid:t,attachment:{filename:a.filename,contentType:a.contentType,size:a.size},outputPath:o,bytesWritten:i.length},null,2)}]}}async handleDeleteMessage(e){await this.ensureRequiredConnections(!0,!1);const t=e.uid;if("number"!=typeof t)throw new Error("uid must be a number");try{return await this.imapClient.deleteMessage(t),{content:[{type:"text",text:`Message with UID ${t} deleted successfully`}]}}catch(e){throw new Error(this.formatError(e,"Failed to delete message"))}}async handleGetMessageCount(){await this.ensureRequiredConnections(!0,!1);try{return{content:[{type:"text",text:`Total messages: ${await this.imapClient.getMessageCount()}`}]}}catch(e){throw new Error(this.formatError(e,"Failed to get message count"))}}async handleGetUnseenMessages(){await this.ensureRequiredConnections(!0,!1);try{const e=await this.imapClient.getUnseenMessages();return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Failed to get unseen messages"))}}async handleGetRecentMessages(){await this.ensureRequiredConnections(!0,!1);try{const e=await this.imapClient.getRecentMessages();return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Failed to get recent messages"))}}async handleGetConnectionStatus(){const e={timestamp:(new Date).toLocaleString("zh-CN",{timeZone:"Asia/Shanghai",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"}),connections:{imap:{connected:!1,currentBox:null,serverInfo:`${u.IMAP.host}:${u.IMAP.port}`,username:u.IMAP.username,tls:u.IMAP.tls,status:"Not connected"},smtp:{connected:!1,serverInfo:`${u.SMTP.host}:${u.SMTP.port}`,username:u.SMTP.username,secure:u.SMTP.secure,status:"Not connected"}}};return this.imapClient&&(e.connections.imap.connected=this.imapClient.isConnected(),e.connections.imap.currentBox=this.imapClient.getCurrentBox(),e.connections.imap.connected?e.connections.imap.status=e.connections.imap.currentBox?`Connected - Current mailbox: ${e.connections.imap.currentBox}`:"Connected - No mailbox open":e.connections.imap.status="Connection lost or failed"),this.smtpClient&&(e.connections.smtp.connected=!0,e.connections.smtp.status="Connected and ready"),{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}async handleSendEmail(e){await this.ensureRequiredConnections(!1,!0);const t={to:e.to.split(",").map(e=>e.trim()),subject:e.subject,text:e.text,html:e.html,cc:e.cc?e.cc.split(",").map(e=>e.trim()):void 0,bcc:e.bcc?e.bcc.split(",").map(e=>e.trim()):void 0,attachments:this.parseAttachments(e.attachments)};if(!t.text&&!t.html)throw new Error("Either text or html content is required");try{const e=await this.smtpClient.sendMail(t);await this.saveSentMessage(t,e.messageId);const n={...e,sentFolderSaved:!0,note:"Email sent successfully and saved to sent folder"};return{content:[{type:"text",text:JSON.stringify(n,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Failed to send email"))}}async handleReplyToEmail(e){await this.ensureRequiredConnections(!0,!0);const t=e.originalUid,n=e.text,r=e.html,s=e.replyToAll||!1,a=!1!==e.includeOriginal;if("number"!=typeof t)throw new Error("originalUid must be a number");try{await this.ensureMailboxOpen(),console.error(`[Reply] Fetching original message with UID: ${t}`);const e=await this.findMessageInMultipleMailboxes(t);if(!e)throw new Error(`Original message with UID ${t} not found in any mailbox`);const o=this.extractEmailFromAddress(e.from);if(!o)throw new Error("Could not extract sender email from original message");let i=[o],c=[];if(s){const t=this.extractEmailsFromAddressField(e.to),n=this.extractEmailsFromAddressField(e.cc),r=[...t,...n].filter(e=>e!==u.IMAP.username&&e!==o);r.length>0&&(c=r)}const l=`Re: ${e.subject||""}`;let d=n,m=r;if(a&&e){const t=e.date?new Date(e.date).toLocaleString():"Unknown Date",s=e.from||"Unknown Sender";if(d=`${n}\n\n${this.buildQuotedText(e.text||"",t,s)}`,r||e.html){const a=this.buildQuotedHtml(e.html||e.text||"",t,s);m=`${r||this.textToHtml(n)}<br><br>${a}`}}const h={to:i,cc:c.length>0?c:void 0,subject:l,text:d,html:m};console.error(`[Reply] Sending reply to: ${i.join(", ")}${c.length>0?` (CC: ${c.join(", ")})`:""}`);const p=await this.smtpClient.sendMail(h);await this.saveSentMessage(h,p.messageId);const f={originalUid:t,originalFrom:o,originalSubject:e.subject,replyToAll:s,includeOriginal:a,recipients:{to:i,cc:c.length>0?c:void 0}},g={...p,replyInfo:f,sentFolderSaved:!0,note:"Reply sent successfully and saved to sent folder"};return{content:[{type:"text",text:JSON.stringify(g,null,2)}]}}catch(e){throw new Error(this.formatError(e,"Failed to reply to email"))}}extractEmailFromAddress(e){if(!e)return null;if(Array.isArray(e)&&e.length>0)return e[0].address||e[0];if("string"==typeof e){const t=e.match(/<([^>]+)>/)||e.match(/([^\s<>]+@[^\s<>]+)/);return t?t[1]:e}return e.address||null}extractEmailsFromAddressField(e){if(!e)return[];if(Array.isArray(e))return e.map(e=>e.address||e).filter(Boolean);if("string"==typeof e)return e.split(",").map(e=>{const t=e.trim().match(/<([^>]+)>/)||e.trim().match(/([^\s<>]+@[^\s<>]+)/);return t?t[1]:e.trim()}).filter(Boolean);const t=this.extractEmailFromAddress(e);return t?[t]:[]}cleanReplySubject(e){return e?e.replace(/^(re:|RE:|回复:|答复:)\s*/i,"").trim():""}detectUnrepliedMessagesAdvanced(e,t,n,r){const s=[],a=[...e].sort((e,t)=>new Date(e.date).getTime()-new Date(t.date).getTime()),o=[...t].sort((e,t)=>new Date(e.date).getTime()-new Date(t.date).getTime());console.error(`[IMAP] Analyzing ${a.length} messages from sender and ${o.length} messages to sender`);const i=Math.min(a.length,100),c=a.slice(0,i);i<a.length&&console.error(`[IMAP] Performance optimization: Processing latest ${i} messages out of ${a.length} total`);for(let e=0;e<c.length;e++){const t=c[e];if(this.isMessageReplied(t,o,n))console.error(`[IMAP] Message UID ${t.uid} (${t.subject}) has been replied`);else if(s.push(t),console.error(`[IMAP] Message UID ${t.uid} (${t.subject}) marked as unreplied`),r&&s.length>=r){console.error(`[IMAP] Early termination: Found ${s.length} unreplied messages (limit: ${r}), stopping further analysis for performance`);break}}return s}isMessageReplied(e,t,n){const r=new Date(e.date),s=this.cleanReplySubject(e.subject||"").toLowerCase();if(t.filter(e=>{const t=new Date(e.date),n=this.cleanReplySubject(e.subject||"").toLowerCase();return t>r&&n===s&&n.length>0}).length>0)return console.error(`[IMAP] Found exact subject match reply for "${s}"`),!0;if(s.length>3&&t.filter(e=>{const t=new Date(e.date),n=this.cleanReplySubject(e.subject||"").toLowerCase();return t>r&&n.includes(s)&&n!==s}).length>0)return console.error(`[IMAP] Found thread-based reply for "${s}"`),!0;const a=new Date(r.getTime()+6048e5);return t.filter(t=>{const n=new Date(t.date);return n>r&&n<=a&&this.isLikelyReplyByContent(e,t)}).length>0&&(console.error(`[IMAP] Found time-window based reply for "${s}"`),!0)}isLikelyReplyByContent(e,t){const n=this.cleanReplySubject(e.subject||"").toLowerCase(),r=this.cleanReplySubject(t.subject||"").toLowerCase();if(n===r&&n.length>0)return!0;if(n.length>5&&r.includes(n))return!0;const s=n.split(/\s+/).filter(e=>e.length>3),a=r.split(/\s+/).filter(e=>e.length>3);return s.length>0&&a.length>0&&s.filter(e=>a.includes(e)).length>=Math.min(2,Math.ceil(s.length/2))}buildQuotedText(e,t,n){return`On ${t}, ${n} wrote:\n${e.split("\n").map(e=>`> ${e}`).join("\n")}`}buildQuotedHtml(e,t,n){return`<div style="border-left: 3px solid #ccc; padding-left: 10px; margin-left: 10px; color: #666;">\n <p><strong>On ${t}, ${n} wrote:</strong></p>\n <div>${e.replace(/\n/g,"<br>")}</div>\n </div>`}textToHtml(e){return e.replace(/\n/g,"<br>").replace(/ /g,"&nbsp;&nbsp;")}async findMessageInMultipleMailboxes(e,t={}){if(this.imapClient.getCurrentBox())try{return await this.imapClient.getMessage(e,t)}catch(e){console.error(`[Search] Message not found in current mailbox: ${e instanceof Error?e.message:String(e)}`)}for(const n of f)try{await this.imapClient.openBox(n,!0);const r=await this.imapClient.getMessage(e,t);return console.error(`[Search] Found message in mailbox: ${n}`),r}catch(e){console.error(`[Search] Message not found in ${n}: ${e instanceof Error?e.message:String(e)}`)}return null}async ensureMailboxOpen(e="INBOX"){this.imapClient.getCurrentBox()||(console.error(`[IMAP] No mailbox currently open, opening ${e}`),await this.imapClient.openBox(e,!0))}async handleConnectAll(){const e=[];try{if(this.imapClient&&this.imapClient.isConnected()){const t=this.imapClient.getCurrentUsername(),n=u.IMAP.username;t!==n?(console.error(`[IMAP] User mismatch: current=${t}, config=${n}, reconnecting...`),await this.imapClient.disconnect(),await this.ensureIMAPConnection(),e.push("✅ IMAP: Reconnected with correct user")):e.push("ℹ️ IMAP: Already connected")}else await this.ensureIMAPConnection(),e.push("✅ IMAP: Connected successfully")}catch(t){e.push(`❌ IMAP: Connection failed - ${t instanceof Error?t.message:String(t)}`)}try{if(this.smtpClient&&this.smtpClient.isConnected()){const t=this.smtpClient.getCurrentUsername(),n=u.SMTP.username;t!==n?(console.error(`[SMTP] User mismatch: current=${t}, config=${n}, reconnecting...`),await this.smtpClient.disconnect(),await this.ensureSMTPConnection(),e.push("✅ SMTP: Reconnected with correct user")):e.push("ℹ️ SMTP: Already connected")}else await this.ensureSMTPConnection(),e.push("✅ SMTP: Connected successfully")}catch(t){e.push(`❌ SMTP: Connection failed - ${t instanceof Error?t.message:String(t)}`)}return{content:[{type:"text",text:e.join("\n")}]}}async handleDisconnectAll(){const e=[];if(this.imapClient)try{await this.imapClient.disconnect(),this.imapClient=null,e.push("✅ IMAP: Disconnected successfully")}catch(t){e.push(`❌ IMAP: Disconnect failed - ${t instanceof Error?t.message:String(t)}`)}else e.push("ℹ️ IMAP: Not connected");if(this.smtpClient)try{await this.smtpClient.disconnect(),this.smtpClient=null,e.push("✅ SMTP: Disconnected successfully")}catch(t){e.push(`❌ SMTP: Disconnect failed - ${t instanceof Error?t.message:String(t)}`)}else e.push("ℹ️ SMTP: Not connected");return{content:[{type:"text",text:e.join("\n")}]}}validateConfig(){try{console.error("=== MCP Mail Server Configuration ==="),console.error(`IMAP: ${u.IMAP.host}:${u.IMAP.port} (TLS: ${u.IMAP.tls})`),console.error(`SMTP: ${u.SMTP.host}:${u.SMTP.port} (Secure: ${u.SMTP.secure})`),console.error(`User: ${u.IMAP.username}`),console.error("Password: [CONFIGURED]"),console.error("Configuration loaded successfully")}catch(e){throw console.error("Configuration error:",e instanceof Error?e.message:String(e)),console.error("Please ensure all required environment variables are set in your MCP server configuration."),e}}buildRawEmailMessage(e,t){const n=(new Date).toUTCString();let r="";r+=`Message-ID: ${t||`<${Date.now()}.${Math.random().toString(36)}@${u.IMAP.host}>`}\r\n`,r+=`Date: ${n}\r\n`,r+=`From: ${u.IMAP.username}\r\n`,r+=`To: ${Array.isArray(e.to)?e.to.join(", "):e.to}\r\n`,e.cc&&(r+=`Cc: ${Array.isArray(e.cc)?e.cc.join(", "):e.cc}\r\n`),e.bcc&&(r+=`Bcc: ${Array.isArray(e.bcc)?e.bcc.join(", "):e.bcc}\r\n`),r+=`Subject: ${e.subject}\r\n`,r+="MIME-Version: 1.0\r\n";const s=!!e.attachments&&e.attachments.length>0,a=`----=_Mixed_${Date.now()}_${Math.random().toString(36).slice(2)}`,o=`----=_Alt_${Date.now()}_${Math.random().toString(36).slice(2)}`;if(s&&(r+=`Content-Type: multipart/mixed; boundary="${a}"\r\n\r\n`,r+=`--${a}\r\n`),e.html&&e.text?(r+=`Content-Type: multipart/alternative; boundary="${o}"\r\n\r\n`,r+=`--${o}\r\n`,r+="Content-Type: text/plain; charset=utf-8\r\n",r+="Content-Transfer-Encoding: 8bit\r\n\r\n",r+=`${e.text}\r\n\r\n`,r+=`--${o}\r\n`,r+="Content-Type: text/html; charset=utf-8\r\n",r+="Content-Transfer-Encoding: 8bit\r\n\r\n",r+=`${e.html}\r\n\r\n`,r+=`--${o}--\r\n`):e.html?(r+="Content-Type: text/html; charset=utf-8\r\n",r+="Content-Transfer-Encoding: 8bit\r\n\r\n",r+=`${e.html}\r\n`):(r+="Content-Type: text/plain; charset=utf-8\r\n",r+="Content-Transfer-Encoding: 8bit\r\n\r\n",r+=`${e.text||""}\r\n`),s&&e.attachments){for(const t of e.attachments){const e=this.normalizeAttachmentBase64(String(t.content),t.encoding||"utf8");r+=`\r\n--${a}\r\n`,r+=`Content-Type: ${t.contentType||"application/octet-stream"}; name="${t.filename}"\r\n`,r+="Content-Transfer-Encoding: base64\r\n",r+=`Content-Disposition: attachment; filename="${t.filename}"\r\n\r\n`,r+=`${e}\r\n`}r+=`--${a}--\r\n`}return r}async saveSentMessage(e,t){try{await this.ensureRequiredConnections(!0,!1);const n=this.buildRawEmailMessage(e,t);await this.imapClient.saveMessageToFolder(n),console.error("[Email] Message saved to sent folder successfully")}catch(e){console.error("[Email] Failed to save message to sent folder:",e instanceof Error?e.message:String(e))}}async run(){const e=new t;await this.server.connect(e),console.error("MCP Mail server running on stdio")}}import.meta.url===`file://${process.argv[1]}`&&(new MailMCPServer).run().catch(console.error);export{MailMCPServer};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-mail-server",
3
- "version": "1.1.12",
3
+ "version": "1.1.14",
4
4
  "description": "MCP server for IMAP/SMTP email access with environment-based configuration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -13,6 +13,7 @@
13
13
  "build:tsc": "tsc",
14
14
  "dev": "tsc --watch",
15
15
  "start": "node dist/index.js",
16
+ "test": "npm run build:tsc && node --test test/*.test.mjs",
16
17
  "prepublishOnly": "npm run build",
17
18
  "clean": "rimraf dist"
18
19
  },