fmaiily
Provides a self-hosted gateway for sending email via Gmail and Google Workspace accounts, enforcing sending limits and offering MCP tools for sending, drafting, and checking quota.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
gmail-automator
Let Claude Code, Cursor, OpenCode, or any AI agent send, read, and reply to Gmail as you - without it ever getting your account locked.
gmail-automator is a self-hosted MCP server for Gmail that exposes 14 tools to AI agents and coding agents over the Model Context Protocol, with a REST API alongside it. It holds the Google OAuth credential so your agent never does, and it accounts for every send against Gmail's real sending quota before contacting Google - refusing a send that would breach the limit instead of discovering the limit by hitting it.
Use it when you want an agent to send email from Claude Code or Cursor, read and triage a Gmail inbox with an LLM, reply to email on your behalf, or automate outreach - and you want the mailbox credential to stay encrypted on your own host.
Protocol | MCP (Model Context Protocol) over Streamable HTTP or stdio; REST API at |
Works with | Claude Code, Claude Desktop, Cursor, Windsurf, OpenCode, Cline, Continue, any MCP client |
License | MIT. Self-hosted. No vendor lock-in, no paid tier, no signup |
Language | Python 3.12+, FastAPI, SQLAlchemy. SQLite by default, PostgreSQL optional |
Credentials | AES-256-GCM encrypted at rest, never returned by the API, never logged |
Default scope |
|
Quota | Enforces Gmail's rolling 24-hour limit at 85%, pacing and backoff included |
Alpha. Pre-1.0: the API can still change. Pin a version if you depend on it. Nothing here is load-bearing for your mail until you point an agent at it.
A restart is not a lost email: the queue is a table with leases, so the next worker resumes.
Docs: Google Cloud setup · Operations runbook · Contributing · Changelog · Security policy · llms.txt
The failure it prevents
Give an agent a Gmail credential and you have also given it a live sending quota. Agents retry. A retry loop looks like a rate limit, then like a lockout, and Gmail's recovery is measured in hours or days. From inside the agent it just looks like the tool is broken.
The usual ways out are worse than the disease: hand the agent full mailbox read scope "just in case", or route mail through someone else's infrastructure. So gmail-automator takes the narrow credential, keeps it, and makes the dangerous send impossible rather than merely discouraged.
Related MCP server: Gmail MCP Server
What it is, and what it is not
Not a mail client, and not a hosted service. It is a quota-aware send proxy with a durable queue in front of the Gmail API. Every path into it - MCP tool or REST route - goes through the same validation, the same quota check, and the same errors, so there is no side door around the policy.
How it compares
Most "let my agent use Gmail" answers point at one of these. Each solves part of the problem.
Approach | Credential lives | Quota enforced | Survives restart | Cost | What it misses |
gmail-automator | on your host, encrypted | before the call, with numbers | durable queue | free, MIT | you run it |
Gmail API called directly by the agent | in the agent | discovered by hitting it | in-flight sends lost | free | the agent can lock you out |
Handing the agent a refresh token | in the agent's context | not enforced | n/a | free | worst option - a leak is a mailbox |
Composio, Zapier, Make, n8n | a SaaS vendor | vendor-side | vendor-side | paid tier | your mail routes through a third party |
Hosted or community Gmail MCP servers | the operator's SaaS | unknown | unknown | paid | same, plus you cannot audit the quota path |
Purpose-built mail API (SendGrid, Postmark, SES) | a vendor key | vendor-side | vendor-side | per send | not Gmail; no inbox, no threads, no labels |
The distinction that matters: everyone else either gives the agent the credential or gives a vendor your mail. This keeps the credential and makes the dangerous send impossible rather than merely discouraged - a send that would cross Gmail's limit is rejected before Google is contacted, with the numbers in the error, so the agent gets a decision to make instead of a lockout to recover from.
If you only need to fire off the occasional message from a script, call the Gmail API directly. This is for the case where the agent is autonomous enough that "the agent overshot the quota" is a real possibility.
What people build with it
Email from a coding agent. A Claude Code session opens a PR, the CI run goes green, and the agent emails the result to the reviewer - without the agent ever holding a token.
Inbox triage. An agent reads unread mail with Gmail's own search syntax (
is:unread,newer_than:7d,from:), labels what it can categorise, and leaves the rest for you.Replies that thread.
replyderives the recipient, subject,In-Reply-To, the wholeReferenceschain, and Gmail'sthreadIdfrom the original, so a reply lands in the same conversation instead of a new one.Outreach that respects the limit. Queue a batch, let pacing spread the sends, and get
messages_remainingback on every response.Human review before anything sends.
draft: true, orcreate_draft, leaves the message in Gmail for a person to approve.
Quickstart
Five steps, and step 5 is a real email in a real inbox. Budget about ten minutes; most of it is Google's consent screen.
1. Run it
git clone https://github.com/instax-dutta/gmail-automator
cd gmail-automator
cp .env.example .env
docker compose up -dIt listens on 127.0.0.1:8000 and nothing else. No Redis, no broker, no database to operate.
uv sync --extra dev
uv run gmail-automator gen-key # -> GMAIL_AUTOMATOR_TOKEN_ENCRYPTION_KEY
cp .env.example .env # fill in the key and your Google OAuth client
uv run gmail-automator migrate
uv run gmail-automator serve # or: gmail-automator mcp-stdio2. Create a Google OAuth client
You need a Cloud project with the Gmail API enabled, then one OAuth client of type Web application. Register this redirect URI exactly:
http://127.0.0.1:8000/v1/oauth/google/callbackPut the client id and secret in .env. Step-by-step, including the part where Google says the app is
unverified and you click through it: docs/google-cloud-setup.md.
3. Connect a mailbox
docker compose exec gmail-automator gmail-automator accounts connectIt prints a consent URL. Open it, approve the scope, and the tokens are stored encrypted. To connect a second mailbox, or to change the scopes later, run it again - Google adds the new permission to the existing grant rather than resetting it.
4. Mint a key for your agent
KEY=$(docker compose exec -T gmail-automator gmail-automator keys create my-agent --scopes send,read | tail -1)Shown once. The secret goes to stdout and the warning to stderr, so the pipe above is safe. Give each
consumer its own key and only the scopes it needs; a key with send alone is refused by
/v1/quota with 403, which is the point.
5. Send a real message
docker compose exec gmail-automator gmail-automator send-test you@gmail.comThat is a genuine send through the whole path: quota check, queue, worker, Gmail. If it arrives, the hard part is done.
Point your agent at it
This is the part that makes it useful. MCP (the Model Context Protocol) is how agent tools are exposed over HTTP, so any MCP client can drive this - Claude Code, Claude Desktop, Cursor, Windsurf, OpenCode, Cline, Continue, Zed, or anything else that speaks MCP.
Streamable HTTP - same process as the API, guarded by the same key:
{
"mcpServers": {
"gmail": {
"url": "http://localhost:8000/mcp",
"headers": { "Authorization": "Bearer fmg_YOUR_KEY_HERE" }
}
}
}Drop that into the MCP config your client reads. Claude Desktop uses
claude_desktop_config.json; Cursor and Windsurf use ~/.cursor/mcp.json and
~/.codeium/windsurf/mcp_config.json; OpenCode and Cline take the same mcpServers shape in their
own config files. Two things that will otherwise cost you an hour:
Use
/mcpwithout a trailing slash./mcp/answers307, which MCP clients do not follow for POST, so it fails with an opaque error.If your client runs on another machine, change the host to wherever the gateway is bound.
stdio - for clients that spawn a subprocess instead:
{
"mcpServers": {
"gmail": { "command": "gmail-automator", "args": ["mcp-stdio"] }
}
}The subprocess inherits the environment, so GMAIL_AUTOMATOR_DATABASE_URL and
GMAIL_AUTOMATOR_TOKEN_ENCRYPTION_KEY must be visible to it. If your client does not pass the
environment through, wrap it:
"command": "sh", "args": ["-c", "GMAIL_AUTOMATOR_DATABASE_URL=... gmail-automator mcp-stdio"].
What your agent can do
Fourteen tools. Each states its own preconditions, and a refusal comes back as a readable result rather than a stack trace - so a model can react instead of retrying blindly.
Send
Tool | Does |
| Send or queue one message; returns a Gmail message id or a job id |
| Up to 50 messages, all-or-nothing |
| Leave it in Gmail for a human to review instead of sending |
Read and organise - need a scope you have to grant
Tool | Does |
| Search with Gmail's own syntax: |
| Decoded headers, body, and the threading headers. Reads HTML-only mail too |
| Replies so it threads in Gmail; |
| Read/unread, star, archive, trash, custom labels |
| Every label with counts, so nothing gets filed under a guess |
Operate
Tool | Does |
| Budget left, when it frees up, queue depth |
| Connected accounts with the scopes each actually holds |
| One job in detail, message id or error code |
| Recent sends, newest first |
| Google consent URL for a new mailbox |
| Revoke and delete that mailbox's tokens |
The agent does not need to know your limits in advance. Every send response carries
messages_remaining, every refusal arrives with the numbers, and get_quota_status is there for
planning. Limits live in the gateway, so the numbers reach the model from one source and cannot go
stale.
There is no permanent delete. modify_message can trash, and that is all.
REST API
Base path /v1. Everything requires Authorization: Bearer <key> except /health, /status,
/metrics, and the Google OAuth callback - the last because a browser redirect cannot carry a header.
Method | Path | Purpose |
|
| Liveness, version, account count, queue depth |
|
| Server-rendered operator dashboard |
|
| Prometheus |
|
| Connected accounts, status, granted scopes |
|
| One account |
|
| Disconnect and delete its tokens |
|
| Google consent URL |
|
| OAuth redirect target |
|
| Disconnect |
|
| Remaining 24h capacity, all accounts |
|
| Remaining 24h capacity, one account |
|
| Send one email, or queue it |
|
| Queue up to 50 emails |
|
| The limits this gateway will actually enforce |
|
| Status of one send |
|
| Recent sends, newest first |
|
| Create a draft instead of sending |
|
| Search the mailbox |
|
| Read one message |
|
| Every label, system and custom |
|
| Change a message's labels |
|
| Reply, or draft the reply, threaded correctly |
Interactive docs at /docs.
Send
curl -s http://localhost:8000/v1/send \
-H "authorization: Bearer $KEY" \
-H 'content-type: application/json' \
-d '{
"to": ["someone@example.com"],
"cc": ["team@example.com"],
"subject": "Build finished",
"body": "All green. Logs attached.",
"wait": true
}'{
"job_id": 42,
"status": "sent",
"message_id": "18f0a1b2c3d4e5f6",
"account": "you@gmail.com",
"messages_remaining": 424
}wait: true (the default) blocks until the worker has sent it, so you get the Gmail message id
directly. wait: false returns a job id immediately - use it for bulk work, then poll
/v1/jobs/{id} or ask the agent for get_send_status.
Batch
curl -s http://localhost:8000/v1/send/batch \
-H "authorization: Bearer $KEY" -H 'content-type: application/json' \
-d '{"emails": [{"to": ["a@example.com"], "subject": "1", "body": "..."},
{"to": ["b@example.com"], "subject": "2", "body": "..."}],
"wait": false}'The whole batch is validated and quota-checked before anything is queued, so a refusal leaves the
queue untouched. Pacing then spreads the sends
GMAIL_AUTOMATOR_DEFAULT_SEND_INTERVAL_SECONDS apart.
Errors
Every error, on every endpoint, has the same shape - which is what lets an agent parse it:
{"error": {"code": "quota_exceeded",
"message": "daily message soft limit reached for you@gmail.com; 425 of 425 used",
"details": {"account": "you@gmail.com", "resource": "messages",
"used": 425, "soft_limit": 425, "reset_at": "..."}}}invalid_request, unauthorized, forbidden, account_not_found, scope_missing,
quota_exceeded, queue_full, duplicate_request, send_failed,
daily_send_quota_exceeded, attachment_too_large, attachment_path_not_allowed, crypto_error,
internal_error.
How the limit actually works
Gmail allows roughly 500 messages a day on a personal account, 2,000 on Workspace, counted over a rolling 24 hours. Those are Google's numbers, not constants here.
It stops at 85%, not 100%.
500 x 0.85 = 425. The remaining 15% is headroom for sends you make by hand, and for the traffic a human generates without the gateway noticing.The window rolls. Counted from the job table over the last 24 hours - completed sends plus jobs already queued. There is no counter table to drift.
The refusal is free. A send that would cross the line is rejected before Google is called, with the numbers in the error. Your agent gets a decision to make, not a lockout to recover from.
Pacing is scheduled, not slept. Jobs carry a not-before time 2s apart per account, so a worker thread never blocks and the schedule survives a restart.
Backoff, and knowing when to stop.
rateLimitExceededand 5xx retry with2^(attempt-1) + jitter, capped, honouringRetry-After, up toGMAIL_AUTOMATOR_MAX_ATTEMPTS. A daily rejection is terminal on purpose: Google documents that it can stay in force for hours, so retrying would only spend more of what is left.
Every one of these is configuration, and
docs/google-cloud-setup.md records the published figures the defaults
came from.
FAQ
Can an AI agent send email without getting my Gmail account banned?
Yes, and that is the specific problem this exists to solve. Gmail allows roughly 500 messages a day on a personal account and 2,000 on Workspace, counted over a rolling 24 hours. Agents retry, and a retry loop looks like a rate limit, then like a lockout, and Gmail's recovery is measured in hours or days. gmail-automator stops at 85% of the hard limit (425 of 500 on a personal account) and rejects any send that would cross the line before Google is contacted, returning the remaining budget in the error.
Does my AI agent ever see my Google OAuth token?
No. The refresh token is AES-256-GCM encrypted at rest, bound to the account address as associated
data so it cannot be moved between accounts, and is never returned by the API or written to a log.
Your agent holds a scoped gateway API key instead (fmg_...), stored as a prefix plus a SHA-256 hash
and shown exactly once.
Which AI coding tools can use this?
Any MCP client. There are ready-made configs for Claude Code, Claude Desktop, Cursor, Windsurf,
OpenCode, Cline, Continue, and Zed - all of them read the same mcpServers JSON shown above. There
is also a plain REST API at /v1 if you would rather call it from a script.
Can the agent read my Gmail, or is it send-only?
Send-only by default, on purpose. An account connected with gmail.send cannot be read: the
read_message, list_messages, reply, and modify_message tools refuse with scope_missing
rather than quietly doing nothing. To let an agent into the mailbox, widen
GMAIL_AUTOMATOR_SOOGLE_OAUTH_SCOPES to gmail.modify (or gmail.readonly for reading alone) and
reconnect. Widening it is a real change in what the gateway can reach, not a formality.
Is there a hosted version?
No, and that is the trade. The credential never leaves your host. It runs in Docker or a systemd
unit, binds 127.0.0.1:8000 by default, and needs no Redis, no broker, and no database you have to
operate.
What happens if the service restarts mid-send?
Nothing is lost. The queue is a table with leases, so a queued job is a row: the next worker picks it up and sends it. Pacing is stored as a not-before time per job rather than a sleep, so the schedule survives the restart too.
Does it send real email or simulate it?
Real email, through the whole path: quota check, queue, worker, Gmail. gmail-automator send-test you@gmail.com is a genuine send - if it arrives, the hard part is done.
Can an AI agent reply to an email without breaking the thread?
Yes. reply derives the recipient, the subject, In-Reply-To, the entire existing References
chain, and Gmail's threadId from the original message, so the reply lands in the same
conversation. Pass draft: true to leave it in Gmail for a human to review instead of sending.
Not for you if
Being explicit here saves you an afternoon:
You only want to send. That is the recommended setup, and the read, reply, and label tools will refuse. Widen
GMAIL_AUTOMATOR_OAUTH_SCOPESonly if you actually want an agent inside your mailbox.You want someone else's infrastructure. There is no hosted version. That is the trade: the credential never leaves your host.
You are sending bulk or unsolicited mail. This stays well inside Gmail's limits as a safety margin. It is not permission.
You need multi-tenant isolation. The auth model assumes one operator issuing keys to their own agents. It is not a public SaaS backend.
You need guaranteed delivery or a receipt. A queued job is retried with backoff, and the confirmation is Gmail's own message id. Nothing more.
Configuration
Every setting is an environment variable prefixed GMAIL_AUTOMATOR_;
.env.example is the annotated list. These are the ones that matter first:
Variable | Default | Meaning |
| required | 32 bytes, base64. Lose it and stored tokens are unreadable |
|
| SQLite, or |
|
|
|
|
| Widen to |
|
| Fraction of the hard limit the gateway will use |
|
| Per-account pacing interval |
|
| Bind address |
|
| Run the send worker in this process |
|
| Allow attachments |
| empty | Directories path attachments may be read from |
CLI
gmail-automator serve # HTTP gateway: REST + MCP
gmail-automator mcp-stdio # MCP over stdio
gmail-automator migrate # apply database migrations
gmail-automator gen-key # generate a token encryption key
gmail-automator status [--json] # accounts, remaining capacity, queue depth
gmail-automator send-test <recipient> # send a real message end to end
gmail-automator rotate-keys [--dry-run] # re-encrypt stored tokens under a new key
gmail-automator accounts list|connect|disconnect
gmail-automator keys create|list|revoke
gmail-automator purge-history [--days N]status --json and keys list write JSON to stdout; logs and human messages go to stderr, so
both are safe to pipe.
rotate-keys moves stored tokens onto a new encryption key in three steps - set the new key plus
GMAIL_AUTOMATOR_TOKEN_ENCRYPTION_KEY_OLD, run the command, drop the old key. The runbook has the
order and the failure modes: docs/operations.md.
Operator status page
GET /status renders what gmail-automator status prints: accounts, quota used against the soft
limit, queue depth, and the next send time. Server-rendered, no JavaScript, no external assets, so it
works on a host with no internet access.
Workspace: unattended sending
With a Workspace admin's domain-wide delegation grant, it can send as a Workspace mailbox with no interactive consent - and no refresh token stored at all:
GMAIL_AUTOMATOR_SERVICE_ACCOUNT_KEY_FILE=/run/secrets/sa.json
GMAIL_AUTOMATOR_SERVICE_ACCOUNT_SUBJECT=agent@acme.coThe admin-side steps are in
docs/google-cloud-setup.md. Limits,
pacing, and encryption work exactly as they do for an OAuth account.
Security
OAuth tokens are AES-256-GCM encrypted at rest, bound to the account address as associated data, so a ciphertext cannot be moved to another account and still decrypt. Never returned by the API, never logged.
Scopes are least-privilege and yours to widen. The default is
gmail.sendplusopenidandemail: an account connected that way cannot be read, and the read, reply, and label tools refuse withscope_missingrather than degrading. Reading and replying needgmail.modify(orgmail.readonlyfor reading alone) added toGMAIL_AUTOMATOR_OAUTH_SCOPES, then a reconnect.gmail.modifyalso grants reading message bodies - widening it is a real change in what the gateway can reach, not a formality.API keys are stored as a prefix plus a SHA-256 hash, compared with
hmac.compare_digest, and shown exactly once.Email bodies live only while a job is in flight and are wiped on a terminal state.
The log pipeline redacts secret-looking keys recursively, and a test fails the build if a credential-shaped literal ever reaches the repository.
The container runs as a non-root uid with a single writable volume, and there is no permanent delete on messages.
Your responsibilities
You remain responsible for the Gmail Terms of Service, recipient consent, and anti-spam rules. This does not hide or bypass Gmail's limits; it stays well inside them so your account is not at risk. Do not use it for bulk or unsolicited mail.
Development
uv sync --extra dev
uv run pytest -q # unit + integration, no sockets
uv run pytest -m e2e # end-to-end over real localhost sockets
uv run ruff check . && uv run ruff format --check . && uv run mypy src
./scripts/docker-smoke.sh # build the image and assert the deployed behaviourThe architecture is enforced by tests rather than convention. tests/unit/test_import_boundaries.py
fails the build if anything outside gmail_automator/gmail/client.py imports googleapiclient, if
anything outside gmail_automator/db.py creates an engine, or if a service calls datetime.now()
instead of taking an injected Clock. Google is never contacted in the default test run:
tests/support/fake_gmail_app.py is an in-process fake of the Gmail and OAuth endpoints.
Contributing
Bug reports and pull requests are welcome. Start with CONTRIBUTING.md; the short
version is make check and uv run pytest -m e2e must pass, and commits follow
Conventional Commits. Please read
CODE_OF_CONDUCT.md first.
License
MIT - see LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Email infrastructure for AI agents — send, receive, search, and reply to email over MCP.
Email for AI agents: send, receive with a safety verdict, reply and approve, as MCP tools.
Let AI agents send email from your own Gmail, Microsoft 365 or SMTP inbox, with guardrails.
Give AI agents their own email inboxes: list, read and send mail via scoped, revocable OAuth grants.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables AI applications to send emails, manage drafts, and compose professional emails through Gmail API with secure multi-user OAuth2 authentication and encrypted token storage.-
- AlicenseAqualityDmaintenanceEnables AI agents to search, read, send, and organize Gmail emails via MCP protocol.22208 npm2MIT
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to send Gmail emails, create drafts, and append content to Google Docs through MCP tools. Provides secure OAuth-based integration with Google Workspace.430 npmMIT
- AlicenseNot gradedqualityBmaintenanceEnables MCP-compatible AI agents to create Gmail drafts, send emails, and append content to Google Docs with OAuth-secured authentication.53 npmMIT