VMail MCP
by ongtungduong
README.md
# vmail-mcp
> **Unofficial community project, not affiliated with Viettel.**
An MCP (Model Context Protocol) server that lets AI assistants (Claude, Cursor, ChatGPT, …) read, search, organize and send mail in your **Viettel VMail** mailbox.
Server settings are built in — you only need your VMail address and password:
| | Host | Port | Security |
|---|---|---|---|
| IMAP | `imap.vmail.viettel.vn` | 993 | SSL |
| SMTP | `smtp.vmail.viettel.vn` | 465 | SSL |
> ⚠️ **Check your organization's policy first.** The MCP server talks only to VMail, but the content of the emails your assistant reads is sent to the AI provider (Anthropic, OpenAI, …). Confirm with your information-security team that this is allowed before connecting a work mailbox.
Based on [imap-mcp-server](https://github.com/nikolausm/imap-mcp-server) by Michael Nikolaus (MIT).
## Quick start
> **Requires Node.js 22.12 or newer.** Check with `node --version`.
**1. Add your VMail account** (opens a local setup page in your browser):
```bash
npx -p vmail-mcp vmail-mcp-setup --skip-claude
```
`--skip-claude` skips the optional Claude Desktop auto-configuration (use step 2 instead).
Enter your full VMail address (e.g. `name@viettel.com.vn`) and password, click **Continue**. The wizard tests the connection and stores the credentials AES-256 encrypted in `~/.vmail-mcp/`.
**2. Add the server to your AI client** — see [Configuration](#configuration). For Claude Code:
```bash
claude mcp add vmail -- npx -y vmail-mcp
```
**3. Ask away** — "Show my unread VMail from today", "Reply to Lan's last email and say I'll join".
> 🔒 **Do not type your password into the chat.** `imap_add_account` exists, but anything you type in the chat is sent to the AI provider. Use the setup wizard or environment variables instead.
### Credentials via environment variables
To keep the password out of `~/.vmail-mcp/` entirely, tick **"Do not save; set later using environment variable"** in the wizard, then provide it in your MCP client config. The variable name is derived from the account name (by default your email address), uppercased with every non-alphanumeric character replaced by `_`:
```
IMAP_MCP_ACCOUNT_NAME_VIETTEL_COM_VN_IMAP_PASSWORD
```
The variables are read once at startup, kept only in memory (encrypted) and removed from the process environment.
> Already using `imap-mcp-server`? vmail-mcp keeps its accounts in `~/.vmail-mcp/`, separate from `~/.imap-mcp/`, so add your VMail account again with the wizard.
## Configuration
### Claude Code (CLI)
#### Option A — via npx (no clone/build needed)
```bash
claude mcp add vmail -- npx -y vmail-mcp
```
This always runs the latest published version and requires no local build.
#### Option B — from a local clone
If you use [Claude Code](https://docs.anthropic.com/en/docs/claude-code) in the terminal, add the MCP server with a single command:
**Step 1:** Make sure you have built the project first (`npm install && npm run build`).
**Step 2:** Run this command in your terminal:
```bash
claude mcp add vmail -- node /absolute/path/to/vmail-mcp/dist/index.js
```
> **Important:** Replace `/absolute/path/to/vmail-mcp` with the actual path where you cloned the repository. For example:
> ```bash
> # macOS/Linux example:
> claude mcp add vmail -- node /Users/yourname/vmail-mcp/dist/index.js
>
> # Windows example:
> claude mcp add vmail -- node C:\Users\yourname\vmail-mcp\dist\index.js
> ```
**Step 3:** Verify it was added:
```bash
claude mcp list
```
You should see `vmail` in the list of configured MCP servers. That's it — the IMAP tools are now available in your Claude Code sessions.
> **Tip:** If you want to remove the server later, run:
> ```bash
> claude mcp remove vmail
> ```
### Claude Desktop (GUI App)
Add the IMAP MCP server to your Claude Desktop configuration file:
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
**Option A — via npx (recommended, no clone/build needed):**
```json
{
"mcpServers": {
"vmail": {
"command": "npx",
"args": ["-y", "vmail-mcp"],
"env": {}
}
}
}
```
**Option B — from a local clone:**
```json
{
"mcpServers": {
"vmail": {
"command": "node",
"args": ["/path/to/vmail-mcp/dist/index.js"],
"env": {}
}
}
}
```
### Restricting tool access (read-only mode / allowlist)
By default all tools are exposed. You can restrict which tools the agent sees
using two environment variables (set them under the `env` key of your MCP
config). This is useful when you want to give an assistant **read-only** access
to a mailbox, or expose only a hand-picked subset of tools.
| Variable | Effect |
| --- | --- |
| `IMAP_MCP_READ_ONLY` | When truthy (`1`, `true`, `yes`, `on`), only the safe, read-only tools are registered — searching, reading, listing folders, unread counts, spam analysis. No tool that sends mail, deletes/moves messages, changes flags, or edits accounts is exposed. |
| `IMAP_MCP_ENABLED_TOOLS` | Comma-separated allowlist of tool names — only these are registered. Names are case-insensitive and the `imap_` prefix is optional (`search_emails` ≡ `imap_search_emails`). When set, it takes precedence over `IMAP_MCP_READ_ONLY`. |
**Example — read-only access:**
```json
{
"mcpServers": {
"vmail": {
"command": "npx",
"args": ["-y", "vmail-mcp"],
"env": { "IMAP_MCP_READ_ONLY": "true" }
}
}
}
```
**Example — explicit allowlist:**
```json
{
"mcpServers": {
"vmail": {
"command": "npx",
"args": ["-y", "vmail-mcp"],
"env": { "IMAP_MCP_ENABLED_TOOLS": "imap_search_emails,imap_get_email,imap_get_latest_emails" }
}
}
}
```
The read-only subset is: `imap_list_accounts`, `imap_connect`, `imap_disconnect`,
`imap_test_account`, `imap_search_emails`, `imap_get_email`,
`imap_get_latest_emails`, `imap_download_attachment`, `imap_find_thread_messages`,
`imap_find_email_by_message_id`, `imap_list_folders`, `imap_folder_status`,
`imap_get_unread_count`, `imap_check_spam`, `imap_domain_stats`,
`imap_list_spam_domains`.
## Usage
Once configured, the IMAP MCP server provides the following tools in Claude:
> **Choosing an account.** For the email and folder tools, `accountId` is
> **optional** and backward-compatible. You may instead pass `accountName`, and
> if you only have a **single** account configured you can omit both — that
> account is used by default. With multiple accounts and no selector, the tool
> returns a clear error listing your options (`imap_list_accounts`).
### Account Management
- **imap_add_account**: Add your VMail account (server settings are built in)
```
Example: { "email": "name@viettel.com.vn", "password": "…" }
Parameters:
- email: Full VMail address (also used as the login)
- password: VMail password
- name: Friendly name for the account (optional — defaults to the email)
- sentFolder: Explicit Sent-folder name for sent-mail copies (optional —
only needed when auto-detection fails)
- defaultBcc: Optional BCC address(es) applied automatically to every
outbound send, reply, forward, and draft for this account. Merged with
any per-call `bcc` (duplicates removed case-insensitively)
```
Only `email` and `password` are required. Prefer the setup wizard so the
password is not typed into the chat.
- **imap_update_account**: Update an existing account (fix SMTP settings, rename, etc.)
```
Parameters:
- accountId: ID of the account to update
- name, host, port, user, password, tls, allowStartTLS, email: IMAP fields (all optional)
- smtpHost, smtpPort, smtpSecure, smtpUser, smtpPassword: SMTP fields (optional)
- host and smtpHost must be *.viettel.vn hostnames (any other value is
rejected); they are only needed if Viettel changes the VMail servers
- saveToSent: Save sent emails to the Sent folder (optional)
- sentFolder: Explicit Sent-folder override (optional). Pass an empty string
to clear the override and re-enable auto-detection
- defaultBcc: Optional default BCC address(es) (optional). Pass an empty
string to clear
```
- **imap_list_accounts**: List all configured accounts
- **imap_remove_account**: Remove an account
```
Parameters:
- accountId: ID of the account to remove
```
- **imap_connect**: Connect to an account
```
Parameters:
- accountId OR accountName: Account identifier
```
- **imap_disconnect**: Disconnect from an account
```
Parameters:
- accountId: Account to disconnect
```
### Email Operations
- **imap_search_emails**: Search for emails
```
Parameters:
- accountId: Account ID
- folder: Folder name (default: INBOX; ignored when searchAllFolders is true)
- searchAllFolders: Search across ALL folders at once (default: false).
Skips Trash/Spam/Drafts and non-selectable folders by default. Use when a
message may have been filed/moved/archived and you don't know its folder.
- includeTrash, includeSpam, includeDrafts: Opt those noisy folders back into
a searchAllFolders run (default: false each)
- from, to, subject, body: Search criteria
- since, before: Date filters
- seen, flagged: Status filters
- keywords: Match messages with ANY of these custom keywords (server-side OR).
Read a mailbox's available custom keywords from `imap_folder_status`'s
`customKeywords` field first.
- unKeywords: Exclude messages with ANY of these custom keywords (result has
NONE of them). Same keyword source as `keywords`.
- limit: Max results (default: 50)
- includeBody: Include parsed message body in the response (default: false).
Fetches the RFC822 source once and parses it with mailparser, so you get
uid + body in a single tool call instead of paying the N+1 cost of one
`imap_get_email` per match. Body is rendered per `bodyFormat` and capped
at `bodyMaxLength` per field.
- bodyFormat: How to render the body when `includeBody` is true — `markdown`
(default, clean Markdown via Turndown), `text`, `html`, or `auto`.
- bodyMaxLength: Per-field cap when `includeBody` is true (default: 10000).
```
> With `searchAllFolders`, results include a `folder` field per message plus
> `foldersSearched`, and any folder that failed to open is reported in
> `foldersErrored` (so a 0-result answer is never silently incomplete).
>
> `includeBody` is honored in the single-folder path only. For a
> cross-folder sweep the lightweight header shape is preserved by design —
> pulling RFC822 source for every match across many folders would multiply
> bandwidth and parse cost. Follow up with `imap_get_email` for the specific
> uids whose bodies you need.
>
> On some servers a "flagged"/starred message carries a custom keyword (e.g.
> an Open-Xchange color label or Apple's `$MailFlagBit*`) instead of, or in
> addition to, the `\Flagged` system flag — after any flagged search, check
> each result's `customKeywords` field before concluding a message is or
> isn't flagged.
- **imap_get_email**: Get full email content
```
Parameters:
- accountId: Account ID
- folder: Folder name
- uid: Email UID
- maxContentLength: Max characters for text/html body (default: 10000)
- includeAttachmentText: Include text attachment previews (default: true)
- maxAttachmentTextChars: Max characters per text attachment (default: 100000)
```
- **imap_get_latest_emails**: Get recent emails
```
Parameters:
- accountId: Account ID
- folder: Folder name (default: INBOX)
- count: Number of emails (default: 10)
- includeBody: Include parsed message body (default: false). Same semantics
as the `includeBody` option on `imap_search_emails` — one round-trip
instead of N×`imap_get_email`.
- bodyFormat: `markdown` (default), `text`, `html`, or `auto`.
- bodyMaxLength: Per-field cap (default: 10000).
```
- **imap_mark_as_read/unread**: Change email read status
```
Parameters:
- accountId: Account ID
- folder: Folder name
- uid: Email UID, OR an array of UIDs to flag in one call. Batch uses a
single IMAP STORE so the operation is atomic at the server level — all
UIDs are flagged, or none. Useful when triaging many messages at once.
```
- **imap_flag_email/unflag_email**: Star/unstar an email (sets or clears the IMAP \Flagged system flag — shows as a "star" in Gmail and Apple Mail). Some servers/clients (Open-Xchange, Apple Mail) also set a separate custom keyword (e.g. `$cl_N`, `$MailFlagBit*`) when flagging; unflag only clears `\Flagged`, so if a message still shows as flagged, check `customKeywords` via `imap_get_email` and clear it with `imap_remove_keyword`.
```
Parameters:
- accountId: Account ID
- folder: Folder name
- uid: Email UID
```
- **imap_add_keyword/remove_keyword**: Set or clear an arbitrary *custom* (non-system) IMAP keyword/label on an email, passed through verbatim (e.g. provider color labels like Open-Xchange's `$cl_1`..`$cl_10` or Apple Mail's `$MailFlagBit0`..`$MailFlagBit2`, or any other custom keyword). Backslash-prefixed system flags (e.g. `\Flagged`, `\Seen`, `\Deleted`) are rejected — use the dedicated flag/read tools for those. Not every server permits custom-keyword changes (see the mailbox's PERMANENTFLAGS); if the server rejects or silently ignores the change, the call fails instead of reporting success.
```
Parameters:
- accountId: Account ID
- folder: Folder name
- uid: Email UID
- keyword: IMAP keyword to set/remove (e.g. "$cl_3")
```
- **imap_delete_email**: Delete an email
```
Parameters:
- accountId: Account ID
- folder: Folder name
- uid: Email UID
```
- **imap_move_email**: Move an email from one folder to another
```
Parameters:
- accountId: Account ID
- folder: Source folder name (default: INBOX)
- uid: Email UID, OR an array of UIDs to move in one call. Batch moves are
attributed per-uid in the response (`results[]` with per-uid `uidMap`
and any errors). Single-uid calls return the legacy response shape.
- targetFolder: Destination folder name
- createDestinationIfMissing: Create the destination folder if it does not exist (default: false)
```
- **imap_find_thread_messages**: Find inbox messages that belong to the same conversation threads as messages already sorted into another folder. Uses RFC 3501 HEADER search on In-Reply-To and References — works on any IMAP server.
```
Parameters:
- accountId: Account ID
- sourceFolder: Folder containing the already-sorted thread messages
- searchFolder: Folder to search for related messages (default: INBOX)
- searchReferences: Also match the References header for multi-level threads (default: true)
- includeBody: Include parsed message body for each found thread message
(default: false). Same semantics as the `includeBody` option on
`imap_search_emails` — one round-trip instead of N×`imap_get_email`.
- bodyFormat: `markdown` (default), `text`, `html`, or `auto`.
- bodyMaxLength: Per-field cap (default: 10000).
```
- **imap_download_attachment**: Download an email attachment (returns images inline, extracts text from PDFs, or saves to downloads directory)
```
Parameters:
- accountId: Account ID
- folder: Folder name (default: INBOX)
- uid: Email UID
- filename: Attachment filename or contentId (as listed by imap_get_email; NFC/NFD
spellings of accented characters are treated as equal, and a contentId may
be passed with or without angle brackets)
- savePath: Optional file path to save the attachment to
- extractText: For PDFs, extract and return text content inline (default: true)
```
- **imap_bulk_delete**: Delete multiple emails at once with chunking and auto-reconnection
```
Parameters:
- accountId: Account ID
- folder: Folder name (default: INBOX)
- uids: Array of email UIDs to delete
- chunkSize: Emails to delete per batch (default: 50)
```
- **imap_bulk_delete_by_search**: Search for emails matching criteria and delete them all
```
Parameters:
- accountId: Account ID
- folder: Folder name (default: INBOX)
- from, to, subject: Search criteria (optional)
- before, since: Date filters (optional)
- chunkSize: Emails to delete per batch (default: 50)
- dryRun: Preview what would be deleted without deleting (default: false)
```
At least one concrete criterion (`from`, `to`, `subject`, `before`, or `since`)
is required — a call with no criteria is refused, so it can never match and
delete an entire folder. On servers whose SEARCH is broken (see
Troubleshooting → Search returns nothing) this tool, `imap_delete_spam` and
`imap_delete_by_domain` return an error instead of deleting from a
client-side match.
- **imap_send_email**: Send a new email
```
Parameters:
- accountId: Account ID to send from
- to: Recipient email address(es) — an array, or a single comma-separated string
- subject: Email subject
- text: Plain text content (optional)
- html: HTML content (optional)
- cc: CC recipients (optional)
- bcc: BCC recipients (optional)
- replyTo: Reply-to address (optional)
- attachments: Array of attachments (optional)
- filename: Attachment filename
- content: Base64 encoded content; provide exactly one of `content` or `path`
- path: Readable local file path to attach; provide exactly one of `path` or `content`
- contentType: MIME type (optional; detected from the filename extension when omitted)
- contentDisposition: "attachment" (default) or "inline" — use "inline" for images shown in the HTML body via cid:
- cid: Content-ID for inline attachments; must match the `cid:` value used in an `<img src="cid:...">` tag in `html`
- dryRun: Validate attachments and compose MIME without sending or saving to Sent (optional, default: false)
```
Attachments are validated before SMTP is contacted. Invalid base64, unreadable
paths, missing filenames, ambiguous sources, and inline attachments without
`cid` fail fast. Successful sends and dry-runs return `attachmentCount` and
safe `attachmentDiagnostics`: filename, MIME type, size, source, disposition,
and cid. Diagnostics omit bytes, raw MIME, and local file paths.
For large files, upload with `imap_upload_file` first and pass its returned
local `path`; for inline images, set `contentDisposition: "inline"` and a
matching `cid`.
After sending, a copy is saved to the account's Sent folder (unless
`saveToSent` is disabled on the account). The folder is resolved via the
account's `sentFolder` override → the server's `\Sent` SPECIAL-USE flag →
a list of known localized names ("Sent", "Gesendet", "Éléments envoyés", …).
The response reports the outcome: `savedToSent` (boolean), `sentFolder`
(the folder used), and — when the save fails — `sentSaveError` explaining
why, instead of failing silently. The same applies to `imap_reply_to_email`
and `imap_forward_email`.
When the account has `defaultBcc` configured, those address(es) are always
BCC'd on send, reply, forward, and draft (merged with any per-call `bcc`;
duplicates removed case-insensitively). The Bcc header is kept in the MIME
stored for drafts and Sent-folder copies so mail clients show it.
- **imap_save_draft**: Save an email as a draft (no send). Takes the same fields as `imap_send_email`, plus `inReplyTo`, `references`, and an optional `folder` override for the Drafts folder.
- **imap_reply_to_email**: Reply to an existing email
```
Parameters:
- accountId: Account ID
- folder: Folder containing the original email
- uid: UID of the email to reply to
- text: Plain text reply content (optional)
- html: HTML reply content (optional)
- replyAll: Reply to all recipients (default: false)
- bcc: BCC recipients (optional; merged with account defaultBcc)
- attachments: Array of attachments (optional, same shape as imap_send_email, including contentDisposition/cid for inline images)
```
- **imap_forward_email**: Forward an existing email
```
Parameters:
- accountId: Account ID
- folder: Folder containing the original email
- uid: UID of the email to forward
- to: Forward to email address(es)
- text: Additional text to include (optional)
- bcc: BCC recipients (optional; merged with account defaultBcc)
- includeAttachments: Include original attachments (default: true)
```
### Folder Operations
- **imap_list_folders**: List all folders
```
Parameters:
- accountId: Account ID
```
Each folder includes its `attributes` (raw IMAP LIST flags) and, when the
server advertises it, `specialUse` — the RFC 6154 role (`\Sent`, `\Drafts`,
`\Trash`, `\Junk`, `\Archive`) that identifies a folder independent of its
localized display name (e.g. "Gesendet" carries `specialUse: "\Sent"`).
- **imap_folder_status**: Get folder information
```
Parameters:
- accountId: Account ID
- folder: Folder name
Returns:
- messages: { total, new, unseen } — from IMAP STATUS
- uidvalidity, uidnext
- flags, permanentFlags: string arrays
- customKeywords: the mailbox's non-system keywords, usable as the
`keywords` / `unKeywords` input of imap_search_emails
```
- **imap_create_folder**: Create a new IMAP folder/mailbox. Most servers also create any missing parent folders. Returns success even if the folder already exists.
```
Parameters:
- accountId: Account ID
- folder: Full folder path to create (e.g. "Archives/2026/2026-05" or "INBOX.Archive")
```
- **imap_get_unread_count**: Count unread emails
```
Parameters:
- accountId: Account ID
- folders: Specific folders (optional)
```
## Security
- Credentials are encrypted using AES-256-CBC encryption
- Encryption keys are stored separately in `~/.vmail-mcp/.key`
- Account configurations are stored in `~/.vmail-mcp/accounts.json`
- The store directory, `.key`, and `accounts.json` are written owner-only
(`0700`/`0600`) so other local users cannot read the key or the credentials
- The web setup wizard's HTTP API never returns stored passwords to the browser
- Downloaded attachments are confined to the downloads directory; sender-supplied
filenames cannot write outside it
- Never commit or share your encryption key or account configurations
## Development
### Running in Development Mode
```bash
npm run dev
```
### Building
```bash
npm run build
```
### Project Structure
```
src/
├── index.ts # MCP server entry point
├── services/
│ ├── imap-service.ts # IMAP connection management
│ ├── smtp-service.ts # SMTP service for sending emails
│ └── account-manager.ts # Account configuration
├── tools/
│ ├── index.ts # Tool registration
│ ├── account-tools.ts # Account management tools
│ ├── email-tools.ts # Email operation tools (including send/reply/forward)
│ └── folder-tools.ts # Folder operation tools
└── types/
└── index.ts # TypeScript type definitions
```
## Example Usage in Claude
1. **Add an account:**
"Add my VMail account name@viettel.com.vn"
2. **Check new emails:**
"Show me the latest 5 emails from my VMail account"
3. **Search emails:**
"Search for emails from boss@company.com in the last week"
4. **Send an email:**
"Send an email to client@example.com with subject 'Project Update'"
5. **Reply to emails:**
"Reply to the latest email from my boss"
6. **Forward emails:**
"Forward the email with subject 'Meeting Notes' to team@company.com"
7. **Move an email:**
"Move the invoice email from INBOX to my Taxes folder"
8. **Manage folders:**
"List all folders in my email account and show unread counts"
## Troubleshooting
### Connection Issues
- Check that you entered your full VMail address (e.g. name@viettel.com.vn) and password
- Check that your network can reach imap.vmail.viettel.vn:993 and smtp.vmail.viettel.vn:465
- Verify that IMAP is enabled in your email account settings
- For sending emails, ensure your account has SMTP access enabled
### Recipients arriving as `["a@x.com","b@y.com"]`
`to`, `cc`, `bcc`, `references` and `uid` accept either a single value or an
array. In JSON Schema that is an `anyOf`, and some MCP clients drop the `anyOf`
before showing the schema to the model — the field then looks untyped or
string-typed, and the client serializes the model's array into a string. The
server used to pass that string straight to nodemailer, which folded the
literal `[` and `]` into the first and last address, so every recipient was
rejected by the receiving mail server (issue #127).
The server now detects a stringified array and restores it, both when
validating tool input and again before composing the message, and logs a
warning to stderr naming the field. Nothing needs to change on your side. If
you want to bypass the client behavior entirely, pass recipients as one
comma-separated string: `"Alice <alice@example.com>, Bob <bob@example.org>"`.
## License
MIT
Original work © 2024 Michael Nikolaus; modifications © 2026 Ông Tùng Dương.
## Contributing
Contributions are welcome! Open issues and pull requests at <https://github.com/ongtungduong/vmail-mcp>.
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessNo issues