MCP Hub
Back to servers

Email (IMAP/SMTP)

Multi-account IMAP/SMTP email server with 22 tools for reading, sending, searching, and organizing.

Registryglama
Forks
1
Updated
Mar 13, 2026

mcp-server-email

CI codecov Go License: MIT MCP

Multi-account email server for the Model Context Protocol. Gives LLMs full email access — read, send, search, organize — over IMAP/SMTP with connection pooling, rate limiting, and retry. Designed as the remote-email counterpart to apple-bridge (local Mail.app access).

Quick Start

  1. Install

    go install github.com/boutquin/mcp-server-email/cmd/mcp-server-email@latest
    
  2. Create a config file (~/.config/mcp-email/accounts.json)

    For well-known providers (Gmail, Outlook, Yahoo, iCloud, Fastmail, Zoho), host and port are auto-detected from the email domain — just provide credentials:

    [
      {
        "id": "hello",
        "email": "hello@gmail.com",
        "username": "hello@gmail.com",
        "password": "app-password-here"
      }
    ]
    

    For custom mail servers, specify host and port explicitly:

    [
      {
        "id": "work",
        "email": "hello@example.com",
        "imap_host": "mail.example.com",
        "imap_port": 993,
        "smtp_host": "mail.example.com",
        "smtp_port": 465,
        "username": "hello@example.com",
        "password": "app-password-here"
      }
    ]
    
    chmod 600 ~/.config/mcp-email/accounts.json
    
  3. Add to Claude Code (~/.claude.json)

    {
      "mcpServers": {
        "email": {
          "command": "mcp-server-email",
          "env": {
            "EMAIL_CONFIG_FILE": "~/.config/mcp-email/accounts.json"
          }
        }
      }
    }
    
  4. Restart Claude Code — the email_* tools are now available.

Installation

Go Install (recommended for Go developers)

go install github.com/boutquin/mcp-server-email/cmd/mcp-server-email@latest

Homebrew (macOS/Linux)

brew install boutquin/tap/mcp-server-email

Binary Download

Download pre-built binaries for your platform from GitHub Releases.

Available for: Linux (amd64, arm64), macOS (amd64, arm64), Windows (amd64, arm64).

Docker

docker run --rm \
  -e EMAIL_ACCOUNTS='[{"id":"main","email":"user@example.com","imap_host":"mail.example.com","imap_port":993,"smtp_host":"mail.example.com","smtp_port":465,"username":"user@example.com","password":"app-password"}]' \
  ghcr.io/boutquin/mcp-server-email:latest

MCP Bundle (Claude Desktop)

Download the .mcpb file from Releases and open in Claude Desktop.

Build from source

git clone https://github.com/boutquin/mcp-server-email.git
cd mcp-server-email
go build -o mcp-server-email ./cmd/mcp-server-email

Configuration

Accounts are loaded once at startup. Changes require a server restart.

Config file vs environment variable

ApproachBest for
EMAIL_CONFIG_FILE — path to a JSON fileProduction use. File can be permission-locked (chmod 600)
EMAIL_ACCOUNTS — inline JSON in env varTesting, CI, or containerized deployments

If both are set, EMAIL_ACCOUNTS takes precedence.

Account JSON schema

[
  {
    "id": "hello",
    "email": "hello@example.com",
    "imap_host": "mail.example.com",
    "imap_port": 993,
    "smtp_host": "mail.example.com",
    "smtp_port": 465,
    "username": "hello@example.com",
    "password": "app-password-here"
  }
]
FieldRequiredDescription
idYesUnique account identifier
emailYesEmail address
imap_hostNo*IMAP server hostname
imap_portNo*IMAP port (993 = implicit TLS, 143 = STARTTLS)
smtp_hostNo*SMTP server hostname
smtp_portNo*SMTP port (465 = implicit TLS, 587 = STARTTLS)
usernameYesLogin username
passwordYes**App password or account password
use_starttlsNoOverride TLS auto-detection (true/false)
insecure_skip_verifyNoSkip TLS certificate verification (dev/testing)
auth_methodNo"password" (default) or "oauth2"
oauth_client_idNoOAuth2 client ID (required when auth_method is "oauth2")
oauth_client_secretNoOAuth2 client secret
oauth_token_fileNoOverride token file path

*Host and port are auto-detected for well-known providers (see below). Required for custom servers. **Not required when using OAuth2 authentication.

Provider auto-detection

When imap_host/smtp_host are omitted, the server detects settings from the email domain:

ProviderDomainsIMAPSMTP
Gmailgmail.com, googlemail.comimap.gmail.com:993smtp.gmail.com:587
Outlookoutlook.com, hotmail.com, live.comoutlook.office365.com:993smtp.office365.com:587
Yahooyahoo.comimap.mail.yahoo.com:993smtp.mail.yahoo.com:587
iCloudicloud.com, me.com, mac.comimap.mail.me.com:993smtp.mail.me.com:587
Fastmailfastmail.com, fastmail.fmimap.fastmail.com:993smtp.fastmail.com:587
Zohozoho.com, zohomail.comimap.zoho.com:993smtp.zoho.com:587

Explicit host/port in the config always takes precedence over auto-detection.

TLS modes

TLS mode is auto-detected from port:

PortProtocolMode
993IMAPImplicit TLS
143IMAPSTARTTLS
465SMTPImplicit TLS
587SMTPSTARTTLS

Override with "use_starttls": true or "use_starttls": false in the account object. Omit for auto-detection (recommended).

OAuth2 authentication

For providers that support it (Gmail, Outlook), you can use OAuth2 instead of app passwords. This uses the device code flow (RFC 8628) — no browser redirect needed.

  1. Create OAuth2 credentials in the provider's developer console (Google Cloud Console or Azure AD)

  2. Configure the account with auth_method: "oauth2":

    [
      {
        "id": "gmail",
        "email": "user@gmail.com",
        "username": "user@gmail.com",
        "auth_method": "oauth2",
        "oauth_client_id": "your-client-id.apps.googleusercontent.com",
        "oauth_client_secret": "your-client-secret"
      }
    ]
    
  3. On first connection, the server initiates the device code flow — printing a verification URL and code to stderr. Visit the URL and enter the code to authorize.

  4. Tokens are persisted in ~/.config/mcp-email/tokens/ and automatically refreshed. Subsequent connections reuse the stored token without re-authorization.

Supported OAuth2 providers: Gmail (gmail.com, googlemail.com) and Outlook (outlook.com, hotmail.com, live.com).

Environment variables

VariableRequiredDefaultDescription
EMAIL_CONFIG_FILEYes*Path to JSON config file
EMAIL_ACCOUNTSYes*JSON array of account configs (inline)
EMAIL_DEFAULT_ACCOUNTNoFirst accountDefault account ID
EMAIL_IMAP_TIMEOUT_MSNo30000IMAP operation timeout (ms)
EMAIL_SMTP_TIMEOUT_MSNo30000SMTP operation timeout (ms)
EMAIL_IMAP_RATE_LIMITNo60IMAP requests/minute/account
EMAIL_SMTP_RATE_LIMITNo100SMTP sends/hour/account
MAX_ATTACHMENT_SIZE_MBNo18Max size per attachment (MB)
MAX_TOTAL_ATTACHMENT_SIZE_MBNo18Max total attachment size per message (MB)
MAX_DOWNLOAD_SIZE_MBNo25Max attachment download size (MB)
EMAIL_POOL_CLOSE_TIMEOUT_MSNo5000Pool close timeout (ms)
EMAIL_DEBUGNofalseDebug logging to stderr
LOG_LEVELNoinfoLog level: debug, info, warn, error
LOG_FORMATNojsonLog format: json or text

*One of EMAIL_CONFIG_FILE or EMAIL_ACCOUNTS is required.

Config file permissions

The config file contains account passwords. Always restrict access:

chmod 600 ~/.config/mcp-email/accounts.json

Tools (22)

Account & folder tools

ToolDescriptionKey params
email_accountsList configured accounts with connection status
email_foldersList all folders with unread/total countsaccount?
email_folder_createCreate new foldername, account?

Message listing

ToolDescriptionKey params
email_listList messages in folderfolder?, limit?, offset?, includeBody?, account?
email_unreadList unread messagesfolder?, limit?, includeBody?, account?
email_searchSearch subject and bodyquery, from?, to?, since?, before?, folder?, limit?, includeBody?, account?

Message operations

ToolDescriptionKey params
email_getGet full message by IDid
email_read_bodyRead email body with paginationid, offset?, limit?, format?
email_moveMove message to folderid, destination
email_copyCopy message to folderid, destination
email_deleteDelete message (trash or permanent expunge)id, permanent?
email_mark_readMark as read/unreadid, read
email_flagFlag/unflag messageid, flagged
email_replyReply to a message (sets In-Reply-To, References, quotes body)id, body, all?, cc?, bcc?, isHtml?, account?
email_forwardForward a message (re-attaches original attachments)id, to, body?, cc?, bcc?, isHtml?, account?
email_batchBatch operations on multiple messagesaction, ids, destination?, permanent?, read?, flagged?

Attachments & threads

ToolDescriptionKey params
email_attachment_listList attachments on a messageid
email_attachment_getDownload attachment by indexid, index, saveTo?
email_threadGet conversation thread (searches across INBOX, Sent, Archive, and All Mail)id

Send & drafts

ToolDescriptionKey params
email_sendSend via SMTP with optional attachmentsto, subject, body, cc?, bcc?, replyTo?, isHtml?, attachments?, account?
email_draft_createSave draft with optional attachmentsto?, subject?, body?, cc?, bcc?, isHtml?, attachments?, account?
email_draft_sendSend existing draftid

All optional account params default to the configured default account.

Search

email_search searches both subject and body using IMAP SEARCH OR (SUBJECT "q") (BODY "q").

Optional filters narrow the candidate set server-side before body scanning:

FilterFormatExample
fromEmail address or name"alice@example.com"
toEmail address or name"bob@example.com"
sinceYYYY-MM-DD"2026-01-01"
beforeYYYY-MM-DD"2026-02-01"

The existing operation timeout (default 30s) prevents hung body searches on large mailboxes.

Attachments

email_send and email_draft_create accept an attachments parameter — an array of file references on the server host:

{
  "attachments": [
    {"path": "/tmp/report.pdf"},
    {"path": "/tmp/data.csv", "filename": "Q1-data.csv", "content_type": "text/csv"}
  ]
}
ParameterRequiredDescription
pathYesAbsolute file path on the server host
filenameNoOverride display filename (defaults to basename of path)
content_typeNoMIME type (auto-detected from file extension if omitted)

Limits (defaults): 18 MB per file, 18 MB total (pre-base64 encoding; stays under 25 MB SMTP cap after encoding). Configurable via MAX_ATTACHMENT_SIZE_MB and MAX_TOTAL_ATTACHMENT_SIZE_MB environment variables.

Download limit: Attachment downloads (email_attachment_get) are capped at 25 MB by default, configurable via MAX_DOWNLOAD_SIZE_MB.

Validation failures (missing file, non-absolute path, size exceeded) return INVALID_ARGUMENT.

Message IDs

Message IDs are composite strings encoding account, mailbox, and UID:

{account}:{mailbox}:{uid}

Example: hello:INBOX:12345

All CRUD tools (email_get, email_move, email_copy, email_delete, email_mark_read, email_flag, email_draft_send) extract the account and folder from the ID — no separate params needed.

Error Codes

All errors are returned as MCP tool errors with a structured code prefix:

CodeMeaning
AUTH_FAILEDIMAP/SMTP authentication failed
CONNECTION_FAILEDCannot connect to server
ACCOUNT_NOT_FOUNDUnknown account ID
FOLDER_NOT_FOUNDMailbox doesn't exist
MESSAGE_NOT_FOUNDUID not found in mailbox
INVALID_ARGUMENTMissing/invalid parameter (including attachment validation)
TIMEOUTOperation timed out
INTERNALUnexpected server error

Resources

URIDescription
email://statusServer version, account connection state, rate limit configuration

Comparison with apple-bridge

This server and apple-bridge share an Email model and parameter semantics (limit, includeBody, folder, query) so LLMs can work with both interchangeably. Key differences:

Aspectmcp-server-emailapple-bridge
TransportIMAP/SMTP (remote)Mail.app (local)
Tool prefixemail_*mail_*
Message ID{account}:{mailbox}:{uid}RFC 5322 Message-ID
Folder createSupportedNot supported (Mail.app requires UI)
Copy messageSupportedNot supported
Draft sendSupportedNot supported (Mail.app uses compose UI)
Attachments (send)File path on server hostNot yet supported

Development

Prerequisites

Build

go build ./...

Unit tests

make test
# or: go test -race -count=1 ./...

Unit tests use mock implementations of the imap.Operations and smtp.Operations interfaces — no live mail server needed.

Benchmarks

go test -bench=. -benchmem ./...
BenchmarkPackageWhat it measures
BenchmarkPoolGetReleaseimapConnection pool acquire/release cycle
BenchmarkExtractAttachmentsimapMIME attachment extraction
BenchmarkHtmlToTexttoolsHTML-to-plain-text conversion
BenchmarkLimiterAllowretryRate limiter (sequential)
BenchmarkLimiterAllow_ParallelretryRate limiter (concurrent)

Fuzz testing

Fuzz targets ship with seed corpora in testdata/fuzz/ directories. Run a specific target:

go test -fuzz=FuzzParseMessageID ./internal/models/ -fuzztime=30s
TargetPackageWhat it fuzzes
FuzzBuildSearchCriteriaimapIMAP search query builder
FuzzExtractAttachmentByIndeximapAttachment index boundary handling
FuzzExtractContentTypeimapMIME content-type parser
FuzzParseMessageIDmodelsComposite message ID codec
FuzzHtmlToTexttoolsHTML-to-text sanitizer
FuzzSplitAddressestoolsEmail address list splitter

Lint

make lint
# or: golangci-lint run ./...

Integration tests

Integration tests exercise the full IMAP/SMTP stack against a real mail server. They are isolated behind the integration build tag and never run during go test ./....

Mail server: Greenmail

Tests use Greenmail, a lightweight Java mail server packaged as a Docker image. Key details that affect how you run it:

SettingValueWhy it matters
IMAPS port3993Greenmail's SSL IMAP port (not 993). The code auto-detects TLS from port number, so tests explicitly set UseStartTLS=false to force implicit TLS on this non-standard port.
SMTPS port3465Greenmail's SSL SMTP port (not 465). Same UseStartTLS=false override.
Bind address0.0.0.0Greenmail defaults to 127.0.0.1 inside the container, which makes Docker port-mapping silently fail (connections get EOF). You must pass -Dgreenmail.hostname=0.0.0.0.
UsernametestGreenmail uses the local part only (before @) as the login username — not the full email address. If the user is test@example.com, the IMAP/SMTP username is test.
TLS certificatesSelf-signedGreenmail generates self-signed certs. Tests set InsecureSkipVerify: true in the account config to accept them.

Quick start (one command)

make test-integration

This starts a Greenmail container, runs all integration tests, then tears down the container — regardless of pass/fail.

Manual step-by-step

If you need to iterate on tests without restarting the container each time:

  1. Start Greenmail

    docker run -d --name greenmail \
      -p 3465:3465 -p 3993:3993 \
      -e "GREENMAIL_OPTS=-Dgreenmail.setup.test.all -Dgreenmail.users=test:password@example.com -Dgreenmail.hostname=0.0.0.0" \
      greenmail/standalone:2.1.0
    

    Wait ~3 seconds for the JVM to start.

  2. Run integration tests

    TEST_IMAP_HOST=localhost TEST_IMAP_PORT=3993 \
    TEST_SMTP_HOST=localhost TEST_SMTP_PORT=3465 \
    TEST_EMAIL=test@example.com TEST_PASSWORD=password \
      go test -tags=integration -race -v ./...
    
  3. Tear down when done

    docker stop greenmail && docker rm greenmail
    

Test environment variables

VariableRequiredDefaultDescription
TEST_IMAP_HOSTYes*IMAP server hostname. Tests skip if unset.
TEST_IMAP_PORTNo3993IMAPS port
TEST_SMTP_HOSTYes*SMTP server hostname. Tests skip if unset.
TEST_SMTP_PORTNo3465SMTPS port
TEST_EMAILNotest@example.comEmail address for the test account
TEST_USERNAMENoLocal part of TEST_EMAILIMAP/SMTP login username (Greenmail uses local part only)
TEST_PASSWORDNopasswordAccount password

*If the corresponding HOST variable is unset, that test file's tests are skipped with a message (not failed).

What the tests cover

IMAP (internal/imap/integration_test.go — 7 tests):

TestWhat it verifies
ConnectAndListFoldersTLS connection, authentication, folder listing, INBOX exists
SendAndListMessagesSMTP send → IMAP receive round-trip, body content match
SearchBySubjectIMAP SEARCH by subject string
DeleteMessagePermanentFlag as deleted + expunge, verify message is gone
MoveMessageIMAP MOVE to another folder (skips if server lacks MOVE extension)
DraftWorkflowSaveDraft → GetDraft → DeleteDraft lifecycle (skips if no APPENDUID)
MarkReadAndFlagSet read/flagged flags, verify via GetMessage

SMTP (internal/smtp/integration_test.go — 4 tests):

TestWhat it verifies
SendPlainTextPlain-text email delivery, body content verified via IMAP
SendHTMLHTML email delivery, Content-Type verified as text/html
SendWithAttachmentMultipart MIME with attachment, filename verified in metadata
RateLimitTokenConsumptionSending consumes a rate-limit token

Troubleshooting

SymptomCauseFix
EOF or connection reset on connectGreenmail bound to 127.0.0.1 inside containerAdd -Dgreenmail.hostname=0.0.0.0 to GREENMAIL_OPTS
TLS handshake failure / certificate errorSelf-signed certs rejectedTest configs already set InsecureSkipVerify: true — if writing new tests, do the same
Invalid login/passwordUsing full email as usernameGreenmail expects the local part only (test, not test@example.com). Set TEST_USERNAME or let it default.
STARTTLS error on port 3993Using STARTTLS on an implicit-TLS portTest configs set UseStartTLS=false. Don't use ports 3143/3025 (plain, no TLS).
Tests skip with "not set"TEST_IMAP_HOST / TEST_SMTP_HOST not exportedExport the env vars or use the make test-integration target
MoveMessage test skipsGreenmail may not support MOVEExpected — test uses t.Skip()

CI

Integration tests run automatically in GitHub Actions via the integration job in .github/workflows/ci.yml. The job uses a Greenmail service container — no manual Docker setup needed. See the workflow file for the exact configuration.

Coverage

To generate a combined unit + integration coverage report:

# With Greenmail running (see above):
TEST_IMAP_HOST=localhost TEST_IMAP_PORT=3993 \
TEST_SMTP_HOST=localhost TEST_SMTP_PORT=3465 \
TEST_EMAIL=test@example.com TEST_PASSWORD=password \
  go test -tags=integration -race -coverprofile=coverage.out ./...

go tool cover -func=coverage.out | tail -1   # total percentage
go tool cover -html=coverage.out             # open in browser

Architecture

mcp-server-email/
├── cmd/mcp-server-email/     # Entry point
└── internal/
    ├── auth/                 # OAuth2 device code flow, XOAUTH2 SASL, token store
    ├── config/               # Multi-account configuration, provider auto-detection
    ├── imap/                 # IMAP client (split by concern), connection pool, Operations interface
    │   ├── client.go         # Client struct, lifecycle, shared helpers
    │   ├── client_messages.go # List, search, get, attachments
    │   ├── client_folders.go  # Folder ops, role cache
    │   ├── client_drafts.go   # Draft save/get/delete
    │   ├── client_flags.go    # Flags, move, copy, delete
    │   └── pool.go           # Connection pool with configurable close timeout
    ├── log/                  # Structured logging (slog) initialization
    ├── models/               # Email model, message ID codec, error types
    ├── resources/            # email://status resource
    ├── retry/                # Token-bucket rate limiter
    ├── smtp/                 # SMTP client, Operations interface
    └── tools/                # 22 MCP tool handlers + registration

Tool handlers are decoupled from IMAP/SMTP clients via the imap.Operations and smtp.Operations interfaces, enabling comprehensive unit testing with mocks.

Dependencies

This project uses go-imap v2 (currently v2.0.0-beta.8). The v2 API is not yet stable — breaking changes may occur before the v2.0.0 release. We pin the exact version in go.mod and will upgrade promptly when stable is released.

License

MIT

Reviews

No reviews yet

Sign in to write a review