Odel
Email (IMAP/SMTP)

Email (IMAP/SMTP)

Local
@boutquin2GoMITUpdated 5mo ago

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

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