Skip to main content
Glama
MailMCP

MailMCP

by KamranMoazim

MailMCP

A complete mail system for one domain on serverless AWS, plus an MCP server so an AI assistant can search, read and draft your mail.

It receives, sends, stores and searches mail, has a web inbox and an admin console, and runs no servers, containers, VPC or NAT gateway. Everything, including the full-text search index, lives in one DynamoDB table and one S3 bucket.

A stateful mail server splits into managed services plus small, stateless, idempotent functions, and the AI gets exactly the access you grant: drafts by default, sends only with an explicit scope, every call audited.

Architecture

To use the app (accounts, inbox, AI access, admin console), see the user guide.

How it works

Three front doors share one storage layer and meet only in storage and in SQS queues.

  • Mail in. SES receives for the domain. The receipt rule lists every active mailbox and alias, so unknown recipients are rejected at SMTP time. SES writes the raw MIME to raw/ and notifies SNS, which feeds the inbound queue. ingest parses, sanitizes the HTML with DOMPurify, resolves aliases, and writes the message, folder counters and usage in one transaction guarded by attribute_not_exists, so a redelivery is a no-op.

  • Mail out. api builds the MIME once, refuses suppressed addresses, and queues one job per route. Local recipients go straight onto inbound and never touch SES. External jobs go to outbound, where sender takes a SEND#jobId lock before calling SES, so a retried job never sends twice. SES events flow back through a configuration set to ses-events, which sets delivered, bounced or complained and maintains the suppression list.

  • People. One CloudFront distribution serves the React app from web/ and routes /api/* and /admin* to Lambda function URLs that only CloudFront can call (origin access control). Sessions are httpOnly cookies backed by Cognito.

  • Agents. AgentCore Gateway is the MCP server. It validates Cognito access tokens, a request interceptor writes the verified caller into every tool call, a Cedar policy engine permits each tool only with its scope, and mail-tools runs the ten tools.

  • Admins. A server-rendered console creates and deletes mailboxes, sets quotas, manages aliases and redrives dead-letter queues. Its IAM role can only touch MBX#, ALIAS#, USER#, DOMAIN# and SUPPRESS# keys and has no access to the mail bucket, so admins cannot read mail.

Related MCP server: universal-email-mcp

Single-table design

Every read is one Query or GetItem; search is one Query per word plus a BatchGet.

#

Pattern

Used by

Index

Key condition

AP1

Resolve a recipient (alias, then mailbox)

ingest, api

Table

ALIAS#addr / META, else MBX#addr / META

AP2

Mailboxes a user can open

api, mail-tools

Table

USER#sub, begins_with(SK, "MBX#")

AP3

Mailbox settings, quota, usage

api, admin

Table

MBX#addr / META

AP4

Folders with total and unread counts

api, mail-tools

Table

MBX#addr, begins_with(SK, "FOLDER#")

AP5

Messages in a folder, newest first, paged

api, mail-tools

GSI1

MAIL#addr#F#Inbox, descending

AP6

One message

api, mail-tools

Table

MAIL#addr / MSG#id

AP7

A thread, oldest first

api, mail-tools

GSI2

MAIL#addr#T#threadId, ascending

AP8

Search by word prefix

api, mail-tools

Table

IDX#addr#in, begins_with(SK, "W#invo")

AP9

Is an address suppressed?

api

Table

SUPPRESS#addr / META

AP10

Sent message from an SES event

ses-events

Table

SES#sesMessageId / META

AP11

Send lock

sender

Table

conditional put SEND#jobId / LOCK

AP12

All mailboxes on the domain

admin

GSI1

DOMAIN#d#MBX

AP13

Every item of a mailbox

purge

Table

MAIL#addr, then each message's tokens

AP14

MCP audit trail, newest first

api

Table

AUDIT#addr, descending

Entity

PK

SK

GSI1

GSI2

Domain

DOMAIN#d

META

Mailbox

MBX#addr

META

DOMAIN#d#MBX / MBX#addr

Folder

MBX#addr

FOLDER#name

Membership

USER#sub

MBX#addr

Alias

ALIAS#addr

META

Message

MAIL#addr

MSG#id

MAIL#addr#F#folder / TS#iso#id

MAIL#addr#T#thread / TS#iso#id

Search token

IDX#addr#xy

W#word#msgId

Sent lookup

SES#sesId

META

Send lock

SEND#jobId

LOCK (TTL 1 day)

Suppression

SUPPRESS#addr

META

Audit row

AUDIT#addr

TS#iso#tool (TTL 90 days)

  • Search index in the table. One item per distinct word per message, sharded by the word's first two letters so a prefix query stays in one partition. Multi-word search intersects message IDs in the function. Body indexing can be turned off per domain.

  • Tokens on the message. Each message keeps its token list (capped at 300), so deleting a message or a mailbox knows exactly which index items to remove.

  • Idempotent writes. Message ID = hash(SES message ID + mailbox); creates use attribute_not_exists. Transactions that touch shared counters retry on TransactionConflict with jitter.

  • Moves are one update. Only GSI1PK changes; counters move in the same transaction. A weekly job recomputes counters and usage to fix drift.

  • Bodies never sit in DynamoDB. GSIs project list fields only; message items stay under 4 KB.

MCP layer

Tool

Scope

What it does

list_mailboxes

mail/read

Mailboxes the caller can open

list_folders

mail/read

Folders with total and unread counts

list_messages

mail/read

A folder's messages, newest first, paged

search_messages

mail/read

Word-prefix search with folder, sender and date filters

get_message

mail/read

Headers, attachment list, plain-text untrusted_body

get_thread

mail/read

Every message in a thread, oldest first

mark_messages

mail/organize

Read, unread, flagged on up to 50 messages

move_messages

mail/organize

Move up to 50 messages; Trash is as far as it goes

create_draft

mail/draft

New draft or reply, tagged createdBy=mcp

send_draft

mail/send

Send a draft, if the owner allows it

  • Caller identity. The Lambda target only receives tool arguments, so the request interceptor verifies the access token and overwrites a reserved _caller argument on every tools/call. A client-supplied _caller never survives.

  • Sending is off by default. Each mailbox has an MCP mode, drafts-only or send, switched by the owner under Settings: AI access. A Cedar permit lets send_draft through only for tokens with mail/send; Cedar denies by default, so a token without it does not even see the tool. At most 20 MCP sends per mailbox per hour.

  • Untrusted content. Bodies come back as plain text with links and HTML removed, capped at 20,000 characters, in a field named untrusted_body. Tool descriptions say mail content is data, not instructions.

  • Audit. Every tool call writes an AUDIT#addr row with the tool, an argument summary (bodies replaced by their length), the result and the client ID.

  • Schemas. The gateway accepts only type, properties, required, items and description, so tool-schema.json uses nothing else and zod validates the rest in mail-tools.

Features

  • Receive for several mailboxes and aliases per domain, with spam, virus, SPF, DKIM and DMARC verdicts stored on each message; spam goes to Junk, viruses are dropped; unknown recipients are rejected at SMTP time.

  • Web inbox: folders, read and unread, flags, threads, attachments, word-prefix search, compose, reply, reply all, forward, drafts, delivery status.

  • Bounce and complaint tracking with a suppression list; per-mailbox daily send cap of 200.

  • Admin console: mailboxes, owners, quotas, aliases, body indexing, dead-letter redrive, purge on delete.

  • Mail that would exceed a quota waits in the inbound dead-letter queue until an admin raises the quota and redrives it, instead of bouncing to a probably forged sender.

Stack

Folder

What

infra/

CDK app, one stack: dns, storage, mail, web, mcp, admin, monitoring constructs

backend/

TypeScript Lambdas on Node 24 (arm64): ingest, sender, ses-events, purge, recount, api and admin (Hono), mail-tools, mcp-interceptor

client/

React 19 + Vite inbox, hash routes, one stylesheet

scripts/

send-test-mail.sh, get-token.sh, cost-report.sh

Quick start

npm run install:all                     # each package has its own lockfile
cp infra/.env.example infra/.env        # MAIL_DOMAIN (a subdomain) and ALERT_EMAIL
cd infra && npx cdk bootstrap && cd ..  # once per account and region (us-east-1)
npm run infra:deploy                    # builds the client, deploys, writes infra/cdk-outputs.json
# delegate MAIL_DOMAIN: add the NameServers output as NS records at the parent zone
npm run mail:seed -- --mailbox you --alias hello      # domain, mailbox, alias, receipt rule
npm run mail:add-user -- --email you@example.com --mailbox you
npm run mail:add-admin -- --email you@example.com     # optional, for /admin
npm run mail:send-test -- you "Hello"                 # SES mail into your inbox

Open the SiteUrl output and sign in with the temporary password. To connect Claude Code:

TOKEN=$(npm run -s mcp:token)           # opens Cognito sign-in; add -- --send for mail/send
claude mcp add --transport http mailmcp "$GATEWAY_URL" --header "Authorization: Bearer $TOKEN"

Locally, npm run api:dev serves the API on port 4000 against the deployed table and npm run client:dev proxies to it.

Tests

npm run infra:test checks the template: 48 checks, including on-demand billing, everything deleting on destroy, 7-day logs, arm64 and Node 24, no wildcard on the table or bucket, the admin key-prefix condition, the Cedar policies, and the alarms and budget.

npm run backend:test runs 124 checks in 11 suites. Unit suites cover keys, the tokenizer, MIME parsing and sanitizing, notification parsing and tool validation. Live suites run against the deployed stack:

Suite

Checks

What it proves

foundation

6

Domain verified with DKIM, MX and DMARC resolve, table and bucket

receive

12

Real mail becomes items and objects; replaying the SQS message creates no duplicate

inbox

18

Every read pattern through CloudFront, cookies, flags and moves keep counters right

send

11

Local delivery skips SES; SES simulator success, bounce and complaint; one SQS job delivered 4 times sends once

mcp

16

OAuth discovery, Cedar hides and denies send_draft, spoofed _caller ignored, prompt-injection mail stays data, drafts-only, rate limit, audit

admin

14

1,000 messages purged after a delete (6.5 s, one invocation), recount repairs drift, admin role cannot read mail (IAM simulator), DLQ redrive

Costs

No VPC, NAT gateway, OpenSearch, RDS or provisioned capacity. Idle cost is the Route 53 hosted zone ($0.50 a month); Gateway, Policy, Cognito, Lambda, SQS and DynamoDB on-demand bill only per use. AgentCore Gateway is $0.005 per 1,000 calls and Policy $0.025 per 1,000 authorizations. Logs keep 7 days, five alarms stay inside the CloudWatch free tier, and a $5 monthly budget on the project=mailmcp tag emails at 80% actual and 100% forecast. npm run cost:report prints the month's bill by service once Cost Explorer has the tag data.

API

Method

Path

Does

POST, DELETE

/api/session

Sign in (sets cookies), sign out

POST

/api/session/password, /api/session/refresh

First-login password, silent refresh

GET

/api/mailboxes

Who am I and which mailboxes I can open

GET

/api/mailboxes/:addr/folders

Folder counts

GET, PATCH

/api/mailboxes/:addr/messages

List a folder; flag, mark or move up to 50

GET

/api/mailboxes/:addr/messages/:id

Message with sanitized body

GET

/api/mailboxes/:addr/messages/:id/attachments/:n

Presigned download

GET

/api/mailboxes/:addr/threads/:threadId

Thread

GET

/api/mailboxes/:addr/search?q=

Word-prefix search

POST

/api/mailboxes/:addr/uploads

Presigned upload for an attachment

POST, PUT

/api/mailboxes/:addr/drafts[/:id]

Create or edit a draft

POST

/api/mailboxes/:addr/drafts/:id/send

Send a draft

GET, PUT

/api/mailboxes/:addr/ai-access

Audit trail and MCP mode

Known limits

  • Web Functions. The API and admin run as Lambda function URLs behind CloudFront OAC. A probe deployed Lambda Web Functions through CloudFormation, but the IAM action and OAC type CloudFront needs to call an IamAuth endpoint are not documented yet. The Hono apps have no hosting-specific code, so moving needs one new entry file.

  • POST through OAC needs a body hash. CloudFront signs requests to function URLs only if the client sends x-amz-content-sha256. The web app computes it, and the admin console uses a 20-line script to submit its forms, so the console is not strictly script-free.

  • MCP tokens last an hour. Cognito has no dynamic client registration, so clients cannot run the OAuth flow themselves; get-token.sh fetches a token by hand.

  • Receipt rule recipients. The rule lists at most 100 addresses, and the admin console rewrites the list outside CloudFormation, so a change to the rule in CDK resets it until the next mailbox change or npm run mail:seed.

  • Search is basic: word prefixes, every word must match, no ranking, stemming or typo tolerance; very long mails index their first 300 distinct words.

  • Threads follow the first References header, so replies from clients that drop it start a new thread.

  • No IMAP or POP. Phone and desktop mail apps need a protocol layer.

  • Prompt injection is reduced, not solved. Drafts-only mode, the Cedar scope, the rate limit and the audit trail bound the damage.

Build log

Phase

What

1

Hosted zone, SES identity with Easy DKIM, MX, SPF, DMARC, the table and bucket

2

Receipt rule, SNS to SQS, ingest, search tokens, idempotent delivery

3

Cognito, CloudFront, Hono API, React inbox with folders, threads and search

4

Compose, local routing, sender with lock, SES events, suppression

5

AgentCore Gateway, interceptor, Cedar policies, mail-tools, audit and AI access settings

6

Admin console, quotas, aliases, SMTP-time rejection, purge, recount, alarms, budget

Tear down

npm run infra:destroy                   # removes everything, stored mail included

Then remove the NS records for the subdomain at the parent zone.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables AI agents to securely read, search, and send email across IMAP and Microsoft Graph accounts, with scoped tokens, approval flows, and an audit trail.
    1
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI assistants to search, retrieve, and send email across multiple ordinary IMAP, POP3, and SMTP mailboxes, with per-account permissions and safe sending checks.
    Apache 2.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to search, read, organize, draft, send, and schedule email through a scoped token, with actions journaled and reversible. It also supports proposal queues, approval workflows, and undo operations for safe mailbox automation.
    15 npm
    8
    Apache 2.0
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables AI assistants to read, search, send, reply, forward, schedule, organize, and analyze email across multiple IMAP/SMTP accounts, with provider-aware labels, drafts, templates, and bulk operations. Additional capabilities include real-time IMAP IDLE watching with AI triage, verification-code capture, calendar extraction, analytics, and optional Streamable HTTP or OAuth2 access.
    23 npm
    ISC